diff --git a/README.md b/README.md index 22d5c17def..824fb1b002 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ Lastly, here's _**one more feature**_: Pluto notebooks have a `@bind` macro to c
-You don't need to know HTML to use it! The [PlutoUI package](https://github.com/fonsp/PlutoUI.jl) contains basic inputs like sliders and buttons. Pluto's interactivity is very easy to use, you will learn more from the sample notebooks inside Pluto! +You don't need to know HTML to use it! The [PlutoUI package](https://github.com/fonsp/PlutoUI.jl) contains basic inputs like sliders and buttons. Pluto's interactivity is very easy to use, you will learn more from the featured notebooks inside Pluto! -But for those who want to dive deeper - you can use HTML, JavaScript and CSS to write your own widgets! Custom update events can be fired by dispatching a `new CustomEvent("input")`, making it compatible with the [`viewof` operator of observablehq](https://observablehq.com/@observablehq/a-brief-introduction-to-viewof). Have a look at the JavaScript sample notebook inside Pluto! +But for those who want to dive deeper - you can use HTML, JavaScript and CSS to write your own widgets! Custom update events can be fired by dispatching a `new CustomEvent("input")`, making it compatible with the [`viewof` operator of observablehq](https://observablehq.com/@observablehq/a-brief-introduction-to-viewof). Have a look at the JavaScript featured notebook inside Pluto!
@@ -160,9 +160,9 @@ Pluto.jl is open source! Specifically, it is [MIT Licensed](https://github.com/f If you want to reference Pluto.jl in scientific writing, you can use our DOI: [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4792401.svg)](https://doi.org/10.5281/zenodo.4792401) -### Sample notebooks +### Featured notebooks -The included sample notebooks have a more permissive license: the [Unlicense](https://github.com/fonsp/Pluto.jl/blob/main/sample/LICENSE). This means that you can use sample notebook code however you like - you do not need to credit us! +Unless otherwise specified, the included featured notebooks have a more permissive license: the [Unlicense](https://github.com/fonsp/Pluto.jl/blob/main/sample/LICENSE). This means that you can use them however you like - you do not need to credit us! Your notebook files are _yours_, you also do not need to credit us. Have fun! diff --git a/frontend-bundler/package.json b/frontend-bundler/package.json index 0ee5bd1416..c05068d4c9 100644 --- a/frontend-bundler/package.json +++ b/frontend-bundler/package.json @@ -6,8 +6,8 @@ "version": "1.0.0", "description": "", "scripts": { - "start": "cd ../frontend && parcel --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html sample.html", - "build": "cd ../frontend && parcel build --no-source-maps --public-url . --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html sample.html && node ../frontend-bundler/add_sri.js ../frontend-dist/editor.html", + "start": "cd ../frontend && parcel --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html", + "build": "cd ../frontend && parcel build --no-source-maps --public-url . --dist-dir ../frontend-dist --config ../frontend-bundler/.parcelrc editor.html index.html error.jl.html && node ../frontend-bundler/add_sri.js ../frontend-dist/editor.html", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", diff --git a/frontend/common/Binder.js b/frontend/common/Binder.js index 8ac593a9f9..2ecbcd3e99 100644 --- a/frontend/common/Binder.js +++ b/frontend/common/Binder.js @@ -124,10 +124,17 @@ export const start_binder = async ({ setStatePromise, connect, launch_params }) let open_response = new Response() if (launch_params.notebookfile.startsWith("data:")) { - open_response = await fetch(with_token(new URL("notebookupload", binder_session_url)), { - method: "POST", - body: await (await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity }))).arrayBuffer(), - }) + open_response = await fetch( + with_token( + with_query_params(new URL("notebookupload", binder_session_url), { + name: new URLSearchParams(window.location.search).get("name"), + }) + ), + { + method: "POST", + body: await (await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity }))).arrayBuffer(), + } + ) } else { for (const [p1, p2] of [ ["path", launch_params.notebookfile], @@ -145,6 +152,12 @@ export const start_binder = async ({ setStatePromise, connect, launch_params }) } } + if (!open_response.ok) { + let b = await open_response.blob() + window.location.href = URL.createObjectURL(b) + return + } + // Opening a notebook gives us the notebook ID, which means that we have a running session! Time to connect. const new_notebook_id = await open_response.text() diff --git a/frontend/common/Bond.js b/frontend/common/Bond.js index a3a89af925..6efc6b2b4d 100644 --- a/frontend/common/Bond.js +++ b/frontend/common/Bond.js @@ -152,7 +152,7 @@ export const set_bound_elements_to_their_value = (bond_nodes, bond_values) => { export const add_bonds_disabled_message_handler = (bond_nodes, invalidation) => { bond_nodes.forEach((bond_node) => { const listener = (e) => { - if (e.target.closest(".bonds_disabled.offer_binder")) { + if (e.target.closest(".bonds_disabled:where(.offer_binder, .offer_local)")) { open_pluto_popup({ type: "info", source_element: e.target, @@ -163,6 +163,7 @@ export const add_bonds_disabled_message_handler = (bond_nodes, invalidation) => //@ts-ignore window.open_edit_or_run_popup() e.preventDefault() + window.dispatchEvent(new CustomEvent("close pluto popup")) }} >Run this notebook diff --git a/frontend/common/RunLocal.js b/frontend/common/RunLocal.js new file mode 100644 index 0000000000..546c0f7d86 --- /dev/null +++ b/frontend/common/RunLocal.js @@ -0,0 +1,80 @@ +import immer from "../imports/immer.js" +import { BackendLaunchPhase } from "./Binder.js" +import { timeout_promise } from "./PlutoConnection.js" +import { with_query_params } from "./URLTools.js" + +// This file is very similar to `start_binder` in Binder.js + +/** + * + * @param {{ + * launch_params: import("../components/Editor.js").LaunchParameters, + * setStatePromise: any, + * connect: () => Promise, + * }} props + */ +export const start_local = async ({ setStatePromise, connect, launch_params }) => { + try { + if (launch_params.pluto_server_url == null || launch_params.notebookfile == null) throw Error("Invalid launch parameters for starting locally.") + + await setStatePromise( + immer((state) => { + state.backend_launch_phase = BackendLaunchPhase.created + state.disable_ui = false + }) + ) + + const with_token = (x) => String(x) + const binder_session_url = new URL(launch_params.pluto_server_url, window.location.href) + + let open_response + + // We download the notebook file contents, and then upload them to the Pluto server. + const notebook_contents = await ( + await fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity ?? undefined })) + ).arrayBuffer() + + open_response = await fetch( + with_token( + with_query_params(new URL("notebookupload", binder_session_url), { + name: new URLSearchParams(window.location.search).get("name"), + clear_frontmatter: "yesplease", + }) + ), + { + method: "POST", + body: notebook_contents, + } + ) + + if (!open_response.ok) { + let b = await open_response.blob() + window.location.href = URL.createObjectURL(b) + return + } + + const new_notebook_id = await open_response.text() + const edit_url = with_query_params(new URL("edit", binder_session_url), { id: new_notebook_id }) + console.info("notebook_id:", new_notebook_id) + + window.history.replaceState({}, "", edit_url) + + await setStatePromise( + immer((state) => { + state.notebook.notebook_id = new_notebook_id + state.backend_launch_phase = BackendLaunchPhase.notebook_running + }) + ) + console.log("Connecting WebSocket") + + const connect_promise = connect() + await timeout_promise(connect_promise, 20_000).catch((e) => { + console.error("Failed to establish connection within 20 seconds. Navigating to the edit URL directly.", e) + + window.parent.location.href = with_token(edit_url) + }) + } catch (err) { + console.error("Failed to initialize binder!", err) + alert("Something went wrong! 😮\n\nWe failed to open this notebook. Please try again with a different browser, or come back later.") + } +} diff --git a/frontend/components/BinderButton.js b/frontend/components/EditOrRunButton.js similarity index 93% rename from frontend/components/BinderButton.js rename to frontend/components/EditOrRunButton.js index 725ec61122..181008b02e 100644 --- a/frontend/components/BinderButton.js +++ b/frontend/components/EditOrRunButton.js @@ -1,6 +1,25 @@ import { BackendLaunchPhase } from "../common/Binder.js" import { html, useEffect, useState, useRef } from "../imports/Preact.js" +export const RunLocalButton = ({ show, start_local }) => { + //@ts-ignore + window.open_edit_or_run_popup = () => { + start_local() + } + + return html`
+ +
` +} + export const BinderButton = ({ offer_binder, start_binder, notebookfile }) => { const [popupOpen, setPopupOpen] = useState(false) const [showCopyPopup, setShowCopyPopup] = useState(false) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index c265fed812..77759440bb 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -25,19 +25,20 @@ import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea import { PlutoActionsContext, PlutoBondsContext, PlutoJSInitializingContext, SetWithEmptyCallback } from "../common/PlutoContext.js" import { start_binder, BackendLaunchPhase, count_stat } from "../common/Binder.js" import { setup_mathjax } from "../common/SetupMathJax.js" -import { BinderButton } from "./BinderButton.js" +import { BinderButton, RunLocalButton } from "./EditOrRunButton.js" import { slider_server_actions, nothing_actions } from "../common/SliderServerClient.js" import { ProgressBar } from "./ProgressBar.js" import { IsolatedCell } from "./Cell.js" import { RawHTMLContainer } from "./CellOutput.js" import { RecordingPlaybackUI, RecordingUI } from "./RecordingUI.js" import { HijackExternalLinksToOpenInNewTab } from "./HackySideStuff/HijackExternalLinksToOpenInNewTab.js" +import { start_local } from "../common/RunLocal.js" import { FrontMatterInput } from "./FrontmatterInput.js" // This is imported asynchronously - uncomment for development // import environment from "../common/Environment.js" -export const default_path = "..." +export const default_path = "" const DEBUG_DIFFING = false // Be sure to keep this in sync with DEFAULT_CELL_METADATA in Cell.jl @@ -88,6 +89,7 @@ const statusmap = (/** @type {EditorState} */ state, /** @type {LaunchParameters static_preview: state.static_preview, bonds_disabled: !(state.connected || state.initializing || launch_params.slider_server_url != null), offer_binder: state.backend_launch_phase === BackendLaunchPhase.wait_for_user && launch_params.binder_url != null, + offer_local: state.backend_launch_phase === BackendLaunchPhase.wait_for_user && launch_params.pluto_server_url != null, binder: launch_params.binder_url != null && state.backend_launch_phase != null, code_differs: state.notebook.cell_order.some( (cell_id) => state.cell_inputs_local[cell_id] != null && state.notebook.cell_inputs[cell_id].code !== state.cell_inputs_local[cell_id].code @@ -191,6 +193,7 @@ const first_true_key = (obj) => { * preamble_html: string?, * isolated_cell_ids: string[]?, * binder_url: string?, + * pluto_server_url: string?, * slider_server_url: string?, * recording_url: string?, * recording_url_integrity: string?, @@ -288,7 +291,10 @@ export class Editor extends Component { disable_ui: launch_params.disable_ui, static_preview: launch_params.statefile != null, - backend_launch_phase: launch_params.notebookfile != null && launch_params.binder_url != null ? BackendLaunchPhase.wait_for_user : null, + backend_launch_phase: + launch_params.notebookfile != null && (launch_params.binder_url != null || launch_params.pluto_server_url != null) + ? BackendLaunchPhase.wait_for_user + : null, binder_session_url: null, binder_session_token: null, connected: false, @@ -1212,7 +1218,11 @@ patch: ${JSON.stringify( initializing: false, }) // view stats on https://stats.plutojl.org/ - count_stat(`article-view`) + if (this.state.pluto_server_url != null) { + count_stat(`article-view`) + } else { + count_stat(`article-view`) + } } else { this.connect() } @@ -1315,6 +1325,9 @@ patch: ${JSON.stringify( <${PlutoActionsContext.Provider} value=${this.actions}> <${PlutoBondsContext.Provider} value=${this.state.notebook.bonds}> <${PlutoJSInitializingContext.Provider} value=${this.js_init_set}> + <${Scroller} active=${this.state.scroller} /> <${ProgressBar} notebook=${this.state.notebook} backend_launch_phase=${this.state.backend_launch_phase} status=${status}/>
@@ -1408,7 +1421,16 @@ patch: ${JSON.stringify( /> ${ - status.offer_binder + status.offer_local + ? html`<${RunLocalButton} + start_local=${() => + start_local({ + setStatePromise: this.setStatePromise, + connect: this.connect, + launch_params: launch_params, + })} + />` + : status.offer_binder ? html`<${BinderButton} offer_binder=${status.offer_binder} start_binder=${() => diff --git a/frontend/components/Popup.js b/frontend/components/Popup.js index 7b142e08fe..e33e128ffb 100644 --- a/frontend/components/Popup.js +++ b/frontend/components/Popup.js @@ -55,6 +55,10 @@ export const Popup = ({ notebook }) => { set_recent_event(e.detail) } + const close = () => { + set_recent_event(null) + } + useEffect(() => { const onpointerdown = (e) => { if (e.target == null) return @@ -62,19 +66,21 @@ export const Popup = ({ notebook }) => { if (recent_source_element_ref.current == null) return if (recent_source_element_ref.current.contains(e.target)) return - set_recent_event(null) + close() } const onkeydown = (e) => { if (e.key === "Escape") { - set_recent_event(null) + close() } } window.addEventListener("open pluto popup", open) + window.addEventListener("close pluto popup", close) window.addEventListener("pointerdown", onpointerdown) document.addEventListener("keydown", onkeydown) return () => { window.removeEventListener("open pluto popup", open) + window.removeEventListener("close pluto popup", close) window.removeEventListener("pointerdown", onpointerdown) document.removeEventListener("keydown", onkeydown) } diff --git a/frontend/components/welcome/Featured.js b/frontend/components/welcome/Featured.js new file mode 100644 index 0000000000..5457e6904a --- /dev/null +++ b/frontend/components/welcome/Featured.js @@ -0,0 +1,167 @@ +import featured_sources from "../../featured_sources.js" +import _ from "../../imports/lodash.js" +import { html, useEffect, useState } from "../../imports/Preact.js" +import { FeaturedCard } from "./FeaturedCard.js" + +const run = (f) => f() + +/** + * @typedef SourceManifestNotebookEntry + * @type {{ + * id: String, + * hash: String, + * html_path: String, + * statefile_path: String, + * notebookfile_path: String, + * frontmatter: Record, + * }} + */ + +/** + * @typedef SourceManifestCollectionEntry + * @type {{ + * title: String?, + * description: String?, + * tags: Array?, + * }} + */ + +/** + * @typedef SourceManifest + * @type {{ + * notebooks: Record, + * collections: Array?, + * pluto_version: String, + * julia_version: String, + * format_version: String, + * source_url: String, + * title: String?, + * description: String?, + * }} + */ + +const placeholder_data = [ + { + title: "Featured Notebooks", + description: "These notebooks from the Julia community show off what you can do with Pluto. Give it a try, you might learn something new!", + collections: [ + { + title: "Loading...", + tags: [], + }, + ], + notebooks: [], + }, +] + +const offline_html = html` + +` + +export const Featured = () => { + // Option 1: Dynamically load source list from a json: + // const [sources, set_sources] = useState(/** @type{Array<{url: String, integrity: String?}>?} */ (null)) + // useEffect(() => { + // run(async () => { + // const data = await (await fetch("featured_sources.json")).json() + + // set_sources(data.sources) + // }) + // }, []) + + // Option 2: From a JS file. This means that the source list can be bundled together. + const sources = featured_sources.sources + + const [source_data, set_source_data] = useState(/** @type{Array} */ ([])) + + useEffect(() => { + if (sources != null) { + const promises = sources.map(async ({ url, integrity }) => { + const data = await (await fetch(new Request(url, { integrity: integrity ?? undefined }))).json() + + if (data.format_version !== "1") { + throw new Error(`Invalid format version: ${data.format_version}`) + } + + set_source_data((old) => [ + ...old, + { + ...data, + source_url: url, + }, + ]) + }) + + Promise.any(promises).catch((e) => { + console.error("All featured sources failed to load: ", e) + set_waited_too_long(true) + }) + } + }, [sources]) + + useEffect(() => { + if (source_data?.length > 0) { + console.log("Sources:", source_data) + } + }, [source_data]) + + const [waited_too_long, set_waited_too_long] = useState(false) + useEffect(() => { + setTimeout(() => { + set_waited_too_long(true) + }, 8 * 1000) + }, []) + + const no_data = !(source_data?.length > 0) + + return no_data && waited_too_long + ? offline_html + : html` + ${(no_data ? placeholder_data : source_data).map( + (data) => html` + + ` + )} + ` +} + +const collection = (/** @type {SourceManifestNotebookEntry[]} */ notebooks, /** @type {String[]} */ tags) => { + const nbs = notebooks.filter((notebook) => tags.some((t) => (notebook.frontmatter?.tags ?? []).includes(t))) + + return /** @type {SourceManifestNotebookEntry[]} */ (_.sortBy(nbs, [(nb) => Number(nb?.frontmatter?.order), "id"])) +} diff --git a/frontend/components/welcome/FeaturedCard.js b/frontend/components/welcome/FeaturedCard.js new file mode 100644 index 0000000000..b12f0529dc --- /dev/null +++ b/frontend/components/welcome/FeaturedCard.js @@ -0,0 +1,93 @@ +import { base64url_to_base64 } from "../../common/PlutoHash.js" +import { with_query_params } from "../../common/URLTools.js" +import _ from "../../imports/lodash.js" +import { html, useEffect, useState, useMemo } from "../../imports/Preact.js" + +const transparent_svg = "data:image/svg+xml;charset=utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E" + +const str_to_degree = (s) => ([...s].reduce((a, b) => a + b.charCodeAt(0), 0) * 79) % 360 + +/** + * @param {{ + * entry: import("./Featured.js").SourceManifestNotebookEntry, + * source_url: String, + * }} props + */ +export const FeaturedCard = ({ entry, source_url }) => { + const title = entry.frontmatter?.title + const u = (x) => (x == null ? null : new URL(x, source_url).href) + + const href = with_query_params(`editor.html`, { + statefile: u(entry.statefile_path), + notebookfile: u(entry.notebookfile_path), + notebookfile_integrity: `sha256-${base64url_to_base64(entry.hash)}`, + disable_ui: `true`, + pluto_server_url: `.`, + name: title == null ? null : `sample ${title}`, + }) + + const author = author_info(entry.frontmatter) + + return html` + + + ${author?.name == null + ? null + : html` + + `} +

${entry.frontmatter.title}

+

${entry.frontmatter.description}

+
+ ` +} + +/** + * @typedef AuthorInfo + * @type {{ + * name: string?, + * url: string?, + * image: string?, + * }} + */ + +const author_info = (frontmatter) => + author_info_item(frontmatter.author) ?? + author_info_item({ + name: frontmatter.author_name, + url: frontmatter.author_url, + image: frontmatter.author_image, + }) + +/** + * @returns {AuthorInfo?} + */ +const author_info_item = (x) => { + if (x instanceof Array) { + return author_info_item(x[0]) + } else if (x == null) { + return null + } else if (typeof x === "string") { + return { + name: x, + url: null, + image: null, + } + } else if (x instanceof Object) { + let { name, image, url } = x + + if (image == null && url != null) { + image = url + ".png?size=48" + } + + return { + name, + url, + image, + } + } else { + return null + } +} diff --git a/frontend/components/welcome/Open.js b/frontend/components/welcome/Open.js index bb9abdd9e8..3c513fd90a 100644 --- a/frontend/components/welcome/Open.js +++ b/frontend/components/welcome/Open.js @@ -1,5 +1,5 @@ import _ from "../../imports/lodash.js" -import { html, Component, useEffect, useState, useMemo } from "../../imports/Preact.js" +import { html } from "../../imports/Preact.js" import { FilePicker } from "../FilePicker.js" import { PasteHandler } from "../PasteHandler.js" @@ -28,27 +28,22 @@ export const Open = ({ client, connected, CustomPicker, show_samples }) => { } const picker = CustomPicker ?? { - text: "Open from file", + text: "Open a notebook", placeholder: "Enter path or URL...", } - return html`

New session:

- <${PasteHandler} /> -
    - ${show_samples && html`
  • Open a sample notebook
  • `} -
  • Create a new notebook
  • -
  • - ${picker.text}: - <${FilePicker} - key=${picker.placeholder} - client=${client} - value="" - on_submit=${on_open_path} - button_label="Open" - placeholder=${picker.placeholder} - /> -
  • -
` + return html`<${PasteHandler} /> +

${picker.text}

+
+ <${FilePicker} + key=${picker.placeholder} + client=${client} + value="" + on_submit=${on_open_path} + button_label="Open" + placeholder=${picker.placeholder} + /> +
` } // /open will execute a script from your hard drive, so we include a token in the URL to prevent a mean person from getting a bad file on your computer _using another hypothetical intrusion_, and executing it using Pluto diff --git a/frontend/components/welcome/Recent.js b/frontend/components/welcome/Recent.js index 7bcfdfca28..815987456e 100644 --- a/frontend/components/welcome/Recent.js +++ b/frontend/components/welcome/Recent.js @@ -181,7 +181,7 @@ export const Recent = ({ client, connected, remote_notebooks, CustomRecent }) => })} > if (CustomRecent == null) { return html` -

Recent sessions:

-
    +

    My work

    +
    ` diff --git a/frontend/components/welcome/Welcome.js b/frontend/components/welcome/Welcome.js index 4c20bd170b..8733e92b46 100644 --- a/frontend/components/welcome/Welcome.js +++ b/frontend/components/welcome/Welcome.js @@ -6,6 +6,7 @@ import { create_pluto_connection } from "../../common/PlutoConnection.js" import { new_update_message } from "../../common/NewUpdateMessage.js" import { Open } from "./Open.js" import { Recent } from "./Recent.js" +import { Featured } from "./Featured.js" // This is imported asynchronously - uncomment for development // import environment from "../../common/Environment.js" @@ -72,8 +73,26 @@ export const Welcome = () => { const { show_samples, CustomRecent, CustomPicker } = extended_components return html` - <${Open} client=${client_ref.current} connected=${connected} CustomPicker=${CustomPicker} show_samples=${show_samples} /> -
    - <${Recent} client=${client_ref.current} connected=${connected} remote_notebooks=${remote_notebooks} CustomRecent=${CustomRecent} /> +
    +

    welcome to

    + +
    +
    +
    + <${Recent} client=${client_ref.current} connected=${connected} remote_notebooks=${remote_notebooks} CustomRecent=${CustomRecent} /> +
    +
    +
    +
    + <${Open} client=${client_ref.current} connected=${connected} CustomPicker=${CustomPicker} show_samples=${show_samples} /> +
    +
    + ` } diff --git a/frontend/dark_color.css b/frontend/dark_color.css index 85d722c801..6447a4923f 100644 --- a/frontend/dark_color.css +++ b/frontend/dark_color.css @@ -139,7 +139,7 @@ --footer-atag-color: rgb(114, 161, 223); --footer-input-border-color: #6c6c6c; --footer-filepicker-button-color: black; - --footer-filepicker-focus-color: #9d9d9d; + --footer-filepicker-focus-color: #c1c1c1; --footnote-border-color: rgba(114, 225, 231, 0.15); /* undo delete cell*/ @@ -185,6 +185,13 @@ /* Landing colors */ --index-text-color: rgb(199, 199, 199); --index-clickable-text-color: rgb(235, 235, 235); + --index-card-bg: #313131; + --welcome-mywork-bg: var(--header-bg-color); + --welcome-newnotebook-bg: rgb(68 72 102); + --welcome-recentnotebook-bg: #3b3b3b; + --welcome-recentnotebook-border: #6e6e6e; + --welcome-open-bg: hsl(233deg 20% 33%); + --welcome-card-author-backdrop: #0000006b; /* docs binding */ --docs-binding-bg: #323431; diff --git a/frontend/editor.css b/frontend/editor.css index 93978ededc..3ea685ec19 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -1069,7 +1069,7 @@ pluto-output:not(.rich_output) > div > pre { display: flex; } -.bonds_disabled.offer_binder bond { +.bonds_disabled:where(.offer_binder, .offer_local) bond { opacity: 0.6; filter: grayscale(1); } @@ -1209,6 +1209,7 @@ pluto-cell.code_differs > pluto-input > .cm-editor { /* UI */ +button.floating_back_button, .overlay-button button, pluto-cell > button, pluto-input > button, @@ -2226,7 +2227,7 @@ footer a { footer input { margin: 0px; border: 2px solid var(--footer-input-border-color); - background: transparent; + background: var(--white); font-family: inherit; font-size: inherit; border-radius: 3px 0 0 3px; @@ -2606,6 +2607,7 @@ nav#slide_controls > button { padding: 5px; } +button.floating_back_button > span::after, nav#slide_controls > button > span::after { content: " " !important; display: block; @@ -2613,6 +2615,7 @@ nav#slide_controls > button > span::after { width: 30px; background-size: 30px 30px; } +button.floating_back_button > span::after, nav#slide_controls > button.prev > span::after { background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/arrow-back-outline.svg"); filter: var(--image-filters); @@ -2622,6 +2625,18 @@ nav#slide_controls > button.next > span::after { filter: var(--image-filters); } +button.floating_back_button { + display: none; +} + +.static_preview.offer_local button.floating_back_button { + display: flex; + position: fixed; + z-index: 1000; + left: 1em; + top: 1em; +} + /* CODEMIRROR HINTS */ .cm-editor .cm-tooltip { diff --git a/frontend/editor.js b/frontend/editor.js index 72ab6c4155..a923ca7825 100644 --- a/frontend/editor.js +++ b/frontend/editor.js @@ -48,6 +48,8 @@ const launch_params = { //@ts-ignore binder_url: url_params.get("binder_url") ?? window.pluto_binder_url, //@ts-ignore + pluto_server_url: url_params.get("pluto_server_url") ?? window.pluto_pluto_server_url, + //@ts-ignore slider_server_url: url_params.get("slider_server_url") ?? window.pluto_slider_server_url, //@ts-ignore recording_url: url_params.get("recording_url") ?? window.pluto_recording_url, @@ -95,6 +97,13 @@ export const empty_notebook_state = ({ notebook_id }) => ({ nbpkg: null, }) +/** + * + * @param {import("./components/Editor.js").NotebookData} state + * @returns {import("./components/Editor.js").NotebookData} + */ +const without_path_entries = (state) => ({ ...state, path: default_path, shortpath: "" }) + /** * * @param {{ @@ -114,7 +123,7 @@ const EditorLoader = ({ launch_params }) => { ;(async () => { const r = await fetch(new Request(launch_params.statefile, { integrity: launch_params.statefile_integrity })) const data = await read_Uint8Array_with_progress(r, set_statefile_download_progress) - const state = unpack(data) + const state = without_path_entries(unpack(data)) initial_notebook_state_ref.current = state set_ready_for_editor(true) })() diff --git a/frontend/featured_sources.js b/frontend/featured_sources.js new file mode 100644 index 0000000000..250ea2b81c --- /dev/null +++ b/frontend/featured_sources.js @@ -0,0 +1,8 @@ +export default { + sources: [ + { + url: "https://cdn.jsdelivr.net/gh/JuliaPluto/featured@a0158dceac9ec602aaf4d8e0d53ddd10b8b56854/pluto_export.json", + integrity: "sha256-6LMXdEg2AvJcgkO1ne6tf75UFC8Vob6YqRAx6ytrfk8=", + }, + ], +} diff --git a/frontend/img/favicon_unsaturated_bg.svg b/frontend/img/favicon_unsaturated_bg.svg new file mode 100755 index 0000000000..1adc29e099 --- /dev/null +++ b/frontend/img/favicon_unsaturated_bg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/index.css b/frontend/index.css index 007a7028a9..65bceeef8a 100644 --- a/frontend/index.css +++ b/frontend/index.css @@ -7,24 +7,16 @@ @import url("light_color.css"); @import url("dark_color.css"); +@import url("https://cdn.jsdelivr.net/npm/open-props@1.3.16/open-props.min.css"); + * { box-sizing: border-box; } - -#title { - /* position: absolute; */ - /* left: 0; */ - /* right: 0; */ - margin-top: 34vh; - /* bottom: 0; */ - /* margin: auto; */ - display: block; - width: 100%; - text-align: center; +html { + font-size: 17px; } -#title h1, -#title h2 { +#title h1 { font-style: italic; font-size: 2em; letter-spacing: 0.08em; @@ -32,15 +24,13 @@ font-family: "Vollkorn", serif; color: var(--pluto-output-h-color); margin: 0px; - border-bottom: solid 2px var(--rule-color); -} - -#title h2 { - font-size: 1.4em; -} + padding: 4rem 1rem 3rem 1rem; + /* flex: 1 1 auto; */ /* max-width: 920px; */ +text-align: center;} -#title img { +#title h1 img { height: 1.2em; + width: 4.9em; margin-bottom: -0.27em; /* margin-right: -1.5em; */ margin-left: 0.1em; @@ -55,31 +45,24 @@ body { background: var(--main-bg-color); } -main { - width: 15em; - margin: 0 auto; - margin-top: 20vh; - text-align: left; - font-family: "Roboto Mono", monospace; - color: var(--index-text-color); -} - p { - color: var(--index-clickable-text-color); + color: var(--index-text-color); } ul { - padding-left: 0.5em; + padding-left: 0; list-style: none; } li { white-space: nowrap; - margin-bottom: 0.9em; + padding: 0.4em; + border-bottom: 1px solid var(--welcome-recentnotebook-border); } a { color: inherit; + color: var(--index-clickable-text-color); } @@ -96,7 +79,8 @@ a { pluto-filepicker { display: flex; flex-direction: row; - margin-top: 0.3rem; + /* margin-top: 0.3rem; */ + background: var(--white); } pluto-filepicker .cm-editor { @@ -105,16 +89,19 @@ pluto-filepicker .cm-editor { width: 100%; font-style: normal; font-weight: 500; - font-family: "Roboto Mono", monospace; + + font-family: var(--inter-ui-font-stack); font-size: 0.75rem; letter-spacing: 1px; background: none; color: var(--nav-filepicker-color); - border: 2px solid var(--nav-filepicker-border-color); + border: 2px solid var(--footer-filepicker-focus-color); border-radius: 3px; border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; + flex: 1 1 100%; + width: 0px; /* min-width: 0px; */ } pluto-filepicker .cm-scroller { @@ -154,11 +141,14 @@ pluto-filepicker button:disabled { } .cm-tooltip-autocomplete { - max-height: calc(20 * 16px); box-sizing: content-box; z-index: 100; } +.cm-editor .cm-tooltip.cm-tooltip-autocomplete > ul { + max-height: max(3em, min(50dvh, 20em)); +} + .cm-tooltip.cm-completionInfo.cm-completionInfo-right:empty { /* https://github.com/codemirror/codemirror.next/issues/574 */ display: none; @@ -166,10 +156,10 @@ pluto-filepicker button:disabled { .cm-editor .cm-tooltip.cm-tooltip-autocomplete > ul > li { /* this is the line height rounded to an integer to prevent jiggle */ - height: 16px; + height: 18px; overflow-y: hidden; /* font-size: 16px; */ - line-height: 16px; + /* line-height: 16px; */ border-radius: 3px; margin-bottom: unset; } @@ -180,9 +170,7 @@ pluto-filepicker button:disabled { } .cm-editor .cm-completionIcon { - opacity: 1; - width: 1em; - transform: translateY(-1.5px); + display: none; } .cm-completionIcon::before { @@ -215,9 +203,10 @@ pluto-filepicker button:disabled { } .cm-editor .cm-tooltip-autocomplete .cm-completionLabel { - font-family: JuliaMono, Menlo, "Roboto Mono", "Lucida Sans Typewriter", "Source Code Pro", monospace !important; + font-family: var(--inter-ui-font-stack); + font-weight: 400; font-variant-ligatures: none; - font-size: 0.75rem; + font-size: 0.8rem; } body.nosessions ul#new ~ * { @@ -225,7 +214,12 @@ body.nosessions ul#new ~ * { } #recent { - margin-bottom: 8em; + scrollbar-gutter: stable;background: var(--welcome-recentnotebook-bg); /* margin-bottom: 8em; */ + max-height: 16em; + overflow-y: auto; + overflow-x: hidden;border-radius: 0.4rem; + box-shadow: -2px 4px 9px 0px #00000012; + border: 0.2rem solid #d5d5d5; } #recent > li.recent { @@ -244,28 +238,30 @@ body.nosessions ul#new ~ * { color: var(--ui-button-color); } -#recent button > span::after { - display: block; +span.ionicon::after { + display: inline-block; content: " "; - background-size: 17px 17px; - height: 17px; - width: 17px; - margin-bottom: -3px; + background-size: 1rem 1rem; + height: 1rem; + width: 1rem; + margin-bottom: -0.17rem; + filter: var(--image-filters); } #recent li.running button > span::after { background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/close-circle.svg"); - filter: var(--image-filters); } #recent li.recent button > span::after { background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/caret-forward-circle-outline.svg"); - filter: var(--image-filters); } #recent li.transitioning button > span::after { background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/ellipsis-horizontal-outline.svg"); - filter: var(--image-filters); +} + +#recent li.new span::after { + background-image: url("https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/add-outline.svg"); } loading-bar { @@ -294,3 +290,134 @@ body.loading loading-bar { right: 0vw; } } + +:root { + --card-width: 15rem; +} + +featured-card { + --card-color: hsl(var(--card-color-hue), 77%, 82%); + --card-border-radius: 10px; + --card-border-width: 3px; + + display: block; + /* width: var(--card-width); */ + border: var(--card-border-width) solid var(--card-color); + border-radius: var(--card-border-radius); + margin: 10px; + padding-bottom: 0.3rem; + box-shadow: 0px 2px 6px 0px #00000014; + font-family: var(--inter-ui-font-stack); + position: relative; + word-break: break-word; + hyphens: auto; + background: var(--index-card-bg); + max-width: var(--card-width); +} + +.card-list { + display: grid; + /* grid-auto-columns: 50px; */ + place-items: center; + align-items: stretch; + grid-template-columns: repeat(auto-fit, minmax(var(--card-width), 1fr)); + gap: 0rem; + justify-items: stretch; +} + +featured-card .banner img { + --zz: calc(var(--card-border-radius) - var(--card-border-width)); + width: 100%; + /* height: 8rem; */ + aspect-ratio: 3/2; + object-fit: cover; + /* background-color: hsl(16deg 100% 66%); */ + background: var(--card-color); + border-radius: var(--zz) var(--zz) 0 0; + flex: 1 1 200px; + min-width: 0; +} + +featured-card .author img { + --size: 1.6em; + /* margin: 0.4em 0.4em; */ + /* margin-bottom: -0.4em; */ + width: var(--size); + height: var(--size); + object-fit: cover; + border-radius: 100%; + background: #b6b6b6; + display: inline-block; + overflow: hidden; +} + +featured-card a { + text-decoration: none; + /* font-weight: 800; */ +} + +featured-card a.banner { + display: flex; +} + +featured-card .author { + font-weight: 600; +} + +featured-card h3 a { + padding: 0.6em; + padding-bottom: 0; + -webkit-line-clamp: 2; + display: inline-block; + display: -webkit-inline-box; + -webkit-box-orient: vertical; + overflow: hidden; + background: var(--index-card-bg); + border-radius: 0.6em; + /* border-top-left-radius: 0; */ +} + +featured-card p { + margin: 0.3rem 0.8rem; + /* padding-top: 0; */ + /* margin-block: 0; */ + color: #838383; + -webkit-line-clamp: 4; + display: inline-block; + display: -webkit-inline-box; + -webkit-box-orient: vertical; + overflow: hidden; +} + +featured-card h3 { + margin: -1.1rem 0rem 0rem 0rem; +} + +featured-card.big { + grid-column-end: span 2; + grid-row-end: span 2; + /* width: 2000px; */ +} + +featured-card.big .banner img { + height: 16rem; +} + +featured-card.special::before { + content: "New!"; + font-size: 1.4rem; + font-weight: 700; + text-transform: uppercase; + font-style: italic; + display: block; + background: #fcf492; + color: #833bc6; + text-shadow: 0 0 1px #ff6767; + position: absolute; + transform: translateY(calc(-100% - -15px)) rotate(-5deg); + padding: 2px 19px; + left: -9px; + /* right: 51px; */ + /* border: 2px solid #ffca62; */ + pointer-events: none; +} diff --git a/frontend/index.html b/frontend/index.html index a032146059..e7fda8c032 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - âš¡ Pluto.jl âš¡ + Pluto.jl + @@ -29,21 +30,43 @@ - + + + -
    -

    welcome to

    -
    -

    New session:

    - -
    + +
    +

    welcome to

    +
    +
    +

    My work

    + +
    +
    +
    +
    +

    Open a notebook

    +

    Loading...

    +
    +
    +
    diff --git a/frontend/index.js b/frontend/index.js index 15efdd148a..4ba142026d 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -3,4 +3,4 @@ import "./common/NodejsCompatibilityPolyfill.js" import { Welcome } from "./components/welcome/Welcome.js" -render(html`<${Welcome} />`, document.querySelector("main")) +render(html`<${Welcome} />`, document.querySelector("#app")) diff --git a/frontend/light_color.css b/frontend/light_color.css index de08581241..bcfa8a11ea 100644 --- a/frontend/light_color.css +++ b/frontend/light_color.css @@ -189,6 +189,13 @@ --index-text-color: hsl(0, 0, 60); --index-clickable-text-color: hsl(0, 0, 30); --docs-binding-bg: #8383830a; + --index-card-bg: white; + --welcome-mywork-bg: hsl(35deg 66% 93%); + --welcome-newnotebook-bg: whitesmoke; + --welcome-recentnotebook-bg: white; + --welcome-recentnotebook-border: #dfdfdf; + --welcome-open-bg: #fbfbfb; + --welcome-card-author-backdrop: #ffffffb0; /* HTML in codemirror! */ --cm-html-color: #48b685; diff --git a/frontend/sample.html b/frontend/sample.html deleted file mode 100644 index e7fb445210..0000000000 --- a/frontend/sample.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - âš¡ Pluto.jl âš¡ - - - - - - - - - - - - - - - - -
    -

    welcome to

    -
    -

    Samples:

    - -
    -
    -
    - - - \ No newline at end of file diff --git a/frontend/welcome.css b/frontend/welcome.css new file mode 100644 index 0000000000..94eaaa6fc7 --- /dev/null +++ b/frontend/welcome.css @@ -0,0 +1,262 @@ +@import url("https://cdn.jsdelivr.net/npm/inter-ui@3.19.3/inter-latin.css"); + +:root { + --pluto-cell-spacing: 17px; + /* use the value "contextual" to enable contextual ligatures `document.documentElement.style.setProperty('--pluto-operator-ligatures', 'contextual');` + for julia mono see here: https://cormullion.github.io/pages/2020-07-26-JuliaMono/#contextual_and_stylistic_alternates_and_ligatures */ + --pluto-operator-ligatures: none; + --julia-mono-font-stack: JuliaMono, Menlo, "Roboto Mono", "Lucida Sans Typewriter", "Source Code Pro", monospace; + --sans-serif-font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + --lato-ui-font-stack: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Cantarell, Helvetica, Arial, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", system-ui, sans-serif; + --inter-ui-font-stack: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Cantarell, Helvetica, Arial, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", system-ui, sans-serif; + + color-scheme: light dark; +} + +html { + font-family: var(--inter-ui-font-stack); + font-size: 17px; +} + +main { + display: block; + max-width: 1200px; + padding: 1rem; + margin: 0 auto; +} +/* font-size: 1.7em; */ + +header { + background-color: #f5efd2; /* background-image: linear-gradient(to top, white, #fff0), var(--noise-4); */ + /* filter: var(--noise-filter-1); */ + display: flex; + background-size: cover; + justify-content: center; + padding: 1.3rem; +} +header h1 { + font-weight: 500; + + /* font-style: italic; */ + text-align: center; + /* color: white; */ +} + +section#mywork, +section#open { + /* --c1: rgb(255 255 255 / 20%); */ + /* --c2: rgb(255 255 255 / 7%); */ + /* background-color: #92add9; */ + /* --grad-stops: transparent 9%, var(--c1) 10%, var(--c1) 12%, transparent 13%, transparent 29%, var(--c2) 30%, var(--c2) 31%, transparent 32%, transparent 49%, + var(--c2) 50%, var(--c2) 51%, transparent 52%, transparent 69%, var(--c2) 70%, var(--c2) 71%, transparent 72%, transparent 89%, var(--c2) 90%, + var(--c2) 91%, transparent 92%, transparent; */ + /* background-size: 40px 40px; */ + /* background-position: 20px 20px; */ + /* background-image: linear-gradient(360deg, var(--grad-stops)), linear-gradient(90deg, var(--grad-stops)); */ + /* background: #f5f5f6; */ + /* position: relative; */ + /* background: url("https://computationalthinking.mit.edu/Spring21/homepage/bg.svg"); */ /* background-size: cover; */ /* background-position: 0% 70%; */ + background: var(--welcome-mywork-bg); + /* background: var(--header-bg-color); */ + position: relative; +} + +.pluto-logo { + font-style: normal; + font-weight: 800; + color: inherit; + /* padding: 0.3em; */ + display: flex; + flex-direction: row; + padding: 0.5em; + align-items: center; + gap: 0.5ch; + font-family: var(--inter-ui-font-stack); + transform: translateY(0.23em); +} + +.pluto-logo img { + height: 1.2em; + width: 1.2em; +} + +#new { + background: var(--welcome-open-bg); + box-shadow: -2px 4px 9px 0px #00000012; + padding: 1.3rem; + border-radius: 0.6rem; + margin: 1rem; + /* border: 0.3rem solid #d6e0d8; */ + display: flex; + flex-direction: column; + align-content: stretch; +} + +section { + display: flex; /* overflow: hidden; */ /* place-items: center; */ /* margin: 0rem 0rem; */ + + flex-direction: row; + justify-content: center; +} + +section > div { + margin: 1rem 1rem; + max-width: 614px; /* margin: auto; */ + flex: 1 1 auto; +} + +.pluto-logo { + background: white; + border-radius: 0.4em; + display: flex; + flex: 0 1 auto; + transform: none; + font-size: 1.6rem; +} + +section#open { + /* background: #f5f5f6; */ + /* box-shadow: inset 1px 1px 20px red; */ + position: relative; +} + +section#featured > div { + max-width: 900px; +} + +header > div { + max-width: 62rem; /* margin: 0 auto; */ + flex: 1 1 auto; + display: flex; + z-index: 1; +} + +section#mywork::before, +section#open::after { + --c: hsl(196deg 20% 26% / 6%); + content: ""; + height: 50px; + top: 0px; + left: 0; + right: 0; + position: absolute; + display: block; + background: linear-gradient(0deg, transparent, var(--c)); + pointer-events: none; + z-index: 0; +} + +:where(#mywork, #open) h2 { + /* color: black; */ + --off: 4px; + --offm: -4px; + --oc: #ffffff; + /* text-shadow: var(--off) 0 var(--oc), var(--off) var(--off) var(--oc), 0 var(--off) var(--oc), var(--offm) var(--off) var(--oc), var(--offm) 0 var(--oc), + var(--offm) var(--offm) var(--oc), 0 var(--offm) var(--oc), var(--off) var(--offm) var(--oc); */ + display: inline-block; /* background: #fffffffc; */ /* padding: 0.4em; */ + border-radius: 0.4em; + /* color: white; */ /* text-transform: uppercase; */ + margin: 2rem 0rem 0rem 0; +} + +section#open::after { + top: unset; + bottom: 0; + background: linear-gradient(0deg, var(--c), transparent); +} + +div#app { + /* background: url(https://computationalthinking.mit.edu/Spring21/homepage/bg.svg); */ + background-size: cover; + background-position: 0% 77%; +} + +section#featured { + /* background: white; */ +} + +.new a { + text-decoration: none; /* font-weight: 700; */ + font-weight: 500; + font-style: italic; +} + +li.new { + position: sticky; + background: var(--welcome-newnotebook-bg); + top: 0; + z-index: 2; +} + +h1 { + font-size: 2.8rem; + margin-block-end: 0em; +} + +.collection { + margin: 6em 0; +} + +.collection h2 { + font-size: 2.5rem; + font-weight: 600; + margin: 0; +} + +#featured p { + max-width: 54ch; + color: #757575; +} + +.author { + position: absolute; + top: 0.3em; + right: 0.3em; + background: var(--welcome-card-author-backdrop); + /* background: hsl(var(--card-color-hue) 34% 46% / 59%); */ + backdrop-filter: blur(15px); + color: black; + border-radius: 117px; + /* height: 2.5em; */ + padding: 0.3em; + padding-right: 0.8em; + display: flex; +} + +.author a { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.4ch; +} + +#github img { + aspect-ratio: 1; + filter: var(--image-filters); + width: 2rem; +} + +a#github { + display: block; + position: absolute; + top: 0.5rem; + right: 0.5rem; +} + +.show-scrollbar::-webkit-scrollbar { + width: 10px; + opacity: 0.1; +} +.show-scrollbar::-webkit-scrollbar-track { +} +.show-scrollbar::-webkit-scrollbar-thumb { + /* height: 11px; */ + background-color: var(--black); + opacity: 0.6; + border-radius: 1000px; +} +.show-scrollbar::-webkit-scrollbar-thumb:hover { + opacity: 1; +}