diff --git a/crates/repl_ui/src/lib.rs b/crates/repl_ui/src/lib.rs index 75b34810cac..2f207234465 100644 --- a/crates/repl_ui/src/lib.rs +++ b/crates/repl_ui/src/lib.rs @@ -13,7 +13,7 @@ use roc_reporting::report::StyleCodes; use crate::colors::GREEN; -// TODO add link to repl tutorial(does not yet exist). +// TODO add link to repl tutorial (does not yet exist). pub const TIPS: &str = concatcp!( "\nEnter an expression to evaluate, or a definition (like ", BLUE, @@ -21,25 +21,7 @@ pub const TIPS: &str = concatcp!( END_COL, ") to use later.\n\n", if cfg!(target_family = "wasm") { - // In the web REPL, the :quit command doesn't make sense. Just close the browser tab! - // We use Shift-Enter for newlines because it's nicer than our workaround for Unix terminals (see below) - concatcp!( - BLUE, - " - ", - END_COL, - PINK, - "Shift-Enter", - END_COL, - " or ", - PINK, - "Ctrl-Enter", - END_COL, - " makes a newline\n", - BLUE, - " - ", - END_COL, - ":help" - ) + "" // In the web repl, we render tips in the UI around the repl instead of in the repl itself. } else { // We use ctrl-v + ctrl-j for newlines because on Unix, terminals cannot distinguish between Shift-Enter and Enter concatcp!( diff --git a/www/generate_tutorial/src/input/tutorial.md b/www/generate_tutorial/src/input/tutorial.md index 598cb9ed6fb..79885b3b77c 100644 --- a/www/generate_tutorial/src/input/tutorial.md +++ b/www/generate_tutorial/src/input/tutorial.md @@ -1565,6 +1565,8 @@ task = Stdout.line "You just entered: \(text)" ``` +## [Backpassing](#backpassing) {#backpassing} + This `<-` syntax is called _backpassing_. The `<-` is a way to define an anonymous function, just like `\ ... ->` is. Here, we're using backpassing to define two anonymous functions. Here's one of them: diff --git a/www/wip_new_website/InteractiveExample.roc b/www/wip_new_website/InteractiveExample.roc new file mode 100644 index 00000000000..72557fc4efb --- /dev/null +++ b/www/wip_new_website/InteractiveExample.roc @@ -0,0 +1,185 @@ +interface InteractiveExample + exposes [view] + imports [pf.Html.{ pre, samp }, pf.Html.Attributes.{ class }] + +Section : [Desc (List Token) Str, Indent, Outdent, Newline] +Token : [ + Kw Str, + Ident Str, + Str Str, + Num Str, + Comment Str, + Literal Str, + ParensAround (List Token), + Lambda (List Str), + StrInterpolation Str Str Str, +] + +view : Html.Node +view = + output = + # # Select anything here to see an explanation. + # main = + # Path.fromStr "url.txt" + # |> storeEmail + # |> Task.onErr handleErr + # + # storeEmail = \filename -> + # url <- File.readUtf8 filename |> Task.await + # { name, email } <- Http.get url Json.codec |> Task.await + # + # File.writeUtf8 (Path.fromStr "\(name).txt") email + # + # handleUrl = \err -> + # when err is + # HttpErr url _ -> Stderr.line "Error fetching URL \(url)" + # FileReadErr path _ -> Stderr.line "Error reading \(Path.display path)" + # FileWriteErr path _ -> Stderr.line "Error writing \(Path.display path)" + sectionsToStr [ + Desc [Comment "Click anything here to see an explanation.Tap anything here to\n# see an explanation."] "

Comments in Roc begin with a # and go to the end of the line.

", + Newline, + Desc [Ident "main", Kw "="] "

This defines main, which is where our program will begin.

In Roc, all definitions are constant, so writing main = again in the same scope would give an error.

", + Indent, + Desc [Ident "Path.fromStr", Str "\"url.txt\""] "

This converts the string \"url.txt\" into a Path by passing it to Path.fromStr.

Function arguments are separated with whitespace. Parentheses are only needed in nested function calls.

", + Newline, + Desc [Kw "|>", Ident "storeEmail"] "

The pipe operator (|>) is syntax sugar for passing the previous value to the next function in the “pipeline.”

Here, we're taking the value returned by Path.fromStr \"url.txt\" and passing it to storeEmail.

The next |> continues the pipeline.

", + Newline, + Desc [Kw "|>", Ident "Task.onErr", Ident "handleErr"] "

If the task returned by the previous step in the pipeline fails, pass its error to handleErr.

The pipeline essentially does this:

val1 = Path.fromStr \"url.txt\"\nval2 = storeEmail val1\n\nTask.onErr val2 handleErr

It creates a Path from a string, stores an email based on that path, and then does error handling.

", + Outdent, + Newline, + Desc [Ident "storeEmail", Kw "=", Lambda ["filename"]] "

This defines a function named storeEmail.

In Roc, functions are ordinary values, so we assign names to them using = like with any other value.

The \\arg1, arg2 -> syntax begins a function, and the part after -> is the function's body.

", + Indent, + Desc [Ident "url", Kw "<-", Ident "File.readUtf8", Ident "filename", Kw "|>", Ident "Task.await"] "

This reads the contents of the file (as a UTF-8 string) into url.

The <- does backpassing, which is syntax sugar for defining a function. This whole line desugars to:

Task.await\n    (File.readUtf8 filename)\n    \\url ->

The lines after this one form the body of the \\url -> callback, which runs if the file read succeeds.

", + Newline, + Desc [Ident "user", Kw "<-", Ident "Http.get", Ident "url", Ident "Json.codec", Kw "|>", Ident "Task.await"] "

This fetches the contents of the URL and decodes them as JSON.

If the shape of the JSON isn’t compatible with the type of user (based on type inference), this will give a decoding error immediately.

As with all the other lines ending in |> Task.await, if there’s an error, nothing else in storeEmail will be run, and handleErr will end up handling the error.

", + Newline, + Desc [Ident "dest", Kw "=", StrInterpolation "\"" "user.name" ".txt\""] "

The \\(user.name) in this string literal will be replaced with the value in name. This is string interpolation.

Note that this line doesn't end with |> Task.await. Earlier lines needed that because they were I/O tasks, but this is a plain old definition, so there's no task to await.

", + Newline, + Desc [Literal "_"] "

In Roc, if you don’t want to bother naming something, you can always choose the name _.

You can name as many things as you like _, but you can never reference anything named _.

So it’s only useful for when you don’t want to choose a name.

", + Desc [Kw "<-", Ident "File.writeUtf8", ParensAround [Ident "Path.fromStr dest"], Ident "user.email", Kw "|>", Ident "Task.await"] "

This writes the user.email string to the file encoded as UTF-8.

The parentheses here show where the nested call to Path.fromStr begins and ends.

", + Newline, + Desc [Ident "Stdout.line", StrInterpolation "\"Wrote email to " "dest" "\""] "

This prints what we did to stdout.

Note that this line doesn't end with |> Task.await. That’s because, although Stdout.line returns a task, we don’t need to await it because nothing happens after it.

", + Outdent, + Newline, + Desc [Ident "handleErr", Kw "=", Lambda ["err"]] "

Like storeEmail, handleErr is also a function.

Although type annotations are optional everywhere in Roc—because the language has 100% type inference—you could add type annotations to main, storeEmail, and handleErr if you wanted to.

", + Indent, + Desc [Kw "when", Ident "err", Kw "is"] "

TODO when

", + Indent, + Desc [Literal "HttpErr", Ident "path", Kw "_", Kw "->"] "

TODO

", + Desc [Ident "Stderr.line", StrInterpolation "\"Error fetching URL " "url" "\""] "

TODO

", + Newline, + Desc [Literal "FileReadErr", Ident "path", Kw "_", Kw "->"] "

TODO

", + Desc [Ident "Stderr.line", StrInterpolation "\"Error reading from " "Path.display path" "\""] "

TODO

", + Newline, + Desc [Literal "FileWriteErr", Ident "path", Kw "_", Kw "->"] "

TODO

", + Desc [Ident "Stderr.line", StrInterpolation "\"Error writing to " "Path.display path" "\""] "

TODO

", + ] + + pre [class "interactive-example"] [ + samp [] [ + Html.text output, + ], + ] + +tokensToStr : List Token -> Str +tokensToStr = \tokens -> + List.walk tokens "" \buf, token -> + bufWithSpace = + if Str.isEmpty buf || token == Literal "," then + buf + else + Str.concat buf " " + + when token is + ParensAround wrapped -> + # Don't put spaces after opening parens or before closing parens + bufWithSpace + |> Str.concat "(" + |> Str.concat (tokensToStr wrapped) + |> Str.concat ")" + + Lambda args -> + # Don't put spaces after opening parens or before closing parens + argsWithCommas = + args + |> List.map \ident -> "\(ident)" + |> Str.joinWith ", " + + bufWithSpace + |> Str.concat "\\" + |> Str.concat argsWithCommas + |> Str.concat " ->" + + Kw str -> + Str.concat bufWithSpace "\(str)" + + Num str | Str str | Literal str -> # We may render these differently in the future + Str.concat bufWithSpace "\(str)" + + Comment str -> + Str.concat bufWithSpace "# \(str)" + + Ident str -> + Str.concat bufWithSpace (identToHtml str) + + StrInterpolation before interp after -> + bufWithSpace + |> Str.concat (if Str.isEmpty before then "" else "\(before)") + |> Str.concat "\\(\(identToHtml interp))" + |> Str.concat (if Str.isEmpty after then "" else "\(after)") + +identToHtml : Str -> Str +identToHtml = \str -> + List.walk (Str.split str ".") "" \accum, ident -> + identHtml = "\(ident)" + + if Str.isEmpty accum then + identHtml + else + "\(accum).\(identHtml)" + +sectionsToStr : List Section -> Str +sectionsToStr = \sections -> + answer = List.walk sections { buf: "", count: 0, indent: 0 } \{ buf, count, indent }, section -> + bufWithSpace = + if Str.isEmpty buf then + buf + else if buf |> Str.endsWith "\n" then + Str.concat buf (Str.repeat " " indent) + else + Str.concat buf " " + + (afterSpaces, nextCount) = + when section is + Newline | Indent | Outdent -> + # Indent and outdent changes happen on the next iteration, + # so we only need a newline for them here. + (Str.concat buf "\n", count) + + Desc tokens str -> + html = radio count (tokensToStr tokens) str + + (Str.concat bufWithSpace html, count + 1) + + nextIndent = + when section is + Indent -> indent + 4 + Outdent -> indent - 4 + Newline | Desc _ _ -> indent + + { + buf: afterSpaces, + count: nextCount, + indent: nextIndent, + } + + answer.buf + +radio : U16, Str, Str -> Str +radio = \index, labelHtml, descHtml -> + # The first radio button should always be checked, and none of the others should be. + checkedHtml = if index == 0 then " checked" else "" + + """ +
\(descHtml)
+ """ diff --git a/www/wip_new_website/build-dev-local.sh b/www/wip_new_website/build-dev-local.sh index a0e0c5a2548..5c355fd5d03 100755 --- a/www/wip_new_website/build-dev-local.sh +++ b/www/wip_new_website/build-dev-local.sh @@ -4,6 +4,12 @@ # NOTE run `bash www/build.sh` to cache local copy of fonts, and repl assets etc +## Get the directory of the currently executing script +DIR="$(dirname "$0")" + +# Change to that directory +cd "$DIR" || exit + rm -rf dist/ cp -r ../build dist/ mkdir -p dist/wip diff --git a/www/wip_new_website/content/index.md b/www/wip_new_website/content/index.md index af6ff72d1e6..db6a5800fed 100644 --- a/www/wip_new_website/content/index.md +++ b/www/wip_new_website/content/index.md @@ -27,23 +27,23 @@
-

Fast

-

Roc code is designed to build fast and run fast. It compiles to machine code or WebAssembly.

+

Fast

+

Roc code is designed to build fast and run fast. It compiles to machine code or WebAssembly.

What does fast mean here?

-

Friendly

-

Roc's syntax, semantics, and toolset are designed to feel user-friendly and helpful.

+

Friendly

+

Roc’s syntax, semantics, and included toolset all prioritize user-friendliness.

What does friendly mean here?

-

Functional

+

Functional

- Roc has a small number of simple language primitives. It's a single-paradigm functional language.

+ Roc has a small number of simple language primitives. It’s a single-paradigm functional language.

What does functional mean here?

@@ -51,20 +51,54 @@ ## Try Roc -
- -
-
Loading REPL WebAssembly module…please wait!
-
-
- +
+ + +
-
- - -## Examples +## Use Cases + +Roc is a very young language (it doesn’t even have a numbered release yet, just nightly builds!) but it can already be used for several things if you’re up for being an early adopter—with all the bugs and missing features which come with that territory. + +Currently these use cases are the best-supported: + +### Command-Line Interfaces (CLIs) + +### Web Servers + +### Embedding + +Calling Roc functions from another language + +### Others + +You can create your own! Learn about **platforms and applications**... + +## Larger Example + +Here’s a larger example that shows a few different aspects of Roc: +* File I/O and HTTP requests +* Pattern matching for error handling +* JSON deserialization via type inference +* Common syntax sugar: string interpolation, pipelines, and backpassing + +The [tutorial](/tutorial) introduces these gradually and in more depth, but this gives you a brief overview. + + diff --git a/www/wip_new_website/main.roc b/www/wip_new_website/main.roc index d77bdd6af7b..41726e4c5c4 100644 --- a/www/wip_new_website/main.roc +++ b/www/wip_new_website/main.roc @@ -3,6 +3,7 @@ app "roc-website" imports [ pf.Html.{ html, head, body, footer, div, main, text, nav, a, link, meta }, pf.Html.Attributes.{ content, name, id, href, rel, lang, class, title, charset, color }, + InteractiveExample, ] provides [transformFileContent] to pf @@ -19,7 +20,7 @@ pageData = getPage : Str -> { title : Str, description : Str } getPage = \current -> Dict.get pageData current - |> Result.withDefault { title: "", description: ""} + |> Result.withDefault { title: "", description: "" } getTitle : Str -> Str getTitle = \current -> @@ -35,6 +36,12 @@ transformFileContent = \page, htmlContent -> view : Str, Str -> Html.Node view = \page, htmlContent -> + mainBody = + if page == "index.html" then + [text htmlContent, InteractiveExample.view] + else + [text htmlContent] + html [lang "en"] [ head [] [ meta [charset "utf-8"] [], @@ -50,15 +57,13 @@ view = \page, htmlContent -> ], body [] [ viewNavbar page, - main [] [ - text htmlContent, - ], + main [] mainBody, footer [] [ div [id "footer"] [ text " powered by ", - a [href "https://www.netlify.com"] [ text "Netlify"], - ] - ] + a [href "https://www.netlify.com"] [text "Netlify"], + ], + ], ], # TODO - add site.js if needed # script [src "/site.js"] [], @@ -81,17 +86,22 @@ viewNavbar = \page -> ], ] +rocLogo : Html.Node rocLogo = - (Html.element "svg") [ + (Html.element "svg") + [ (Html.attribute "viewBox") "0 -6 51 58", (Html.attribute "xmlns") "http://www.w3.org/2000/svg", (Html.attribute "aria-labelledby") "logo-link", (Html.attribute "role") "img", - class "roc-logo" - ] [ + class "roc-logo", + ] + [ (Html.element "title") [id "logo-link"] [text "Return to Roc Home"], - (Html.element "polygon") [ + (Html.element "polygon") + [ (Html.attribute "role") "presentation", (Html.attribute "points") "0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086", - ] [], + ] + [], ] diff --git a/www/wip_new_website/static/repl.css b/www/wip_new_website/static/repl.css index 4814adcabf9..4ba1bf141d4 100644 --- a/www/wip_new_website/static/repl.css +++ b/www/wip_new_website/static/repl.css @@ -1,6 +1,7 @@ #repl { display: flex; flex-direction: column; + font-size: 18px; } #source-input-wrapper { @@ -36,12 +37,12 @@ margin: 0; margin-bottom: 2em; box-sizing: border-box; + font-size: 18px; } #source-input:focus { - border: 1px solid var(--cyan); + outline: 2px solid var(--primary-1); box-sizing: border-box; - outline: none; } .history { @@ -79,7 +80,7 @@ } .panic { - color: red; + color: #ff6666; } .input-line-prefix { @@ -87,7 +88,7 @@ } .color-red { - color: red; + color: #ff6666; } .color-green { @@ -103,7 +104,7 @@ } .color-magenta { - color: var(--magenta); + color: var(--primary-1); } .color-cyan { @@ -130,7 +131,6 @@ text-decoration: underline; } - /* Mobile-friendly screen width */ @media only screen and (max-width: 767px) { #repl { diff --git a/www/wip_new_website/static/repl.js b/www/wip_new_website/static/repl.js index 490d3fb0ebb..8dddf7a0b79 100644 --- a/www/wip_new_website/static/repl.js +++ b/www/wip_new_website/static/repl.js @@ -11,6 +11,8 @@ console.error = function displayErrorInHistoryPanel(string) { import * as roc_repl_wasm from "./roc_repl_wasm.js"; +const isHomepage = document.getElementById("homepage-repl-container") != null; + // ---------------------------------------------------------------------------- // REPL state // ---------------------------------------------------------------------------- @@ -41,9 +43,7 @@ repl.elemSourceInput.addEventListener("keydown", onInputKeydown); repl.elemSourceInput.addEventListener("keyup", onInputKeyup); roc_repl_wasm.default("/wip/roc_repl_wasm_bg.wasm").then(async (instance) => { repl.elemHistory.querySelector("#loading-message").remove(); - repl.elemSourceInput.disabled = false; repl.elemSourceInput.placeholder = "Type some Roc code and press Enter."; - repl.elemSourceInput.focus(); repl.compiler = instance; // Get help text from the compiler, and display it at top of the history panel @@ -165,11 +165,51 @@ async function processInputQueue() { // Callbacks to JS from Rust // ---------------------------------------------------------------------------- +var ROC_PANIC_INFO = null; + +function send_panic_msg_to_js(rocstr_ptr, panic_tag) { + const { memory } = repl.app.exports; + + const rocStrBytes = new Int8Array(memory.buffer, rocstr_ptr, 12); + const finalByte = rocStrBytes[11] + + let stringBytes = ""; + if (finalByte < 0) { + // small string + + // bitwise ops on negative JS numbers are weird. This clears the bit that we + // use to indicate a small string. In rust it's `finalByte as u8 ^ 0b1000_0000` + const length = finalByte + 128; + stringBytes = new Uint8Array(memory.buffer, rocstr_ptr, length); + } else { + // big string + const rocStrWords = new Uint32Array(memory.buffer, rocstr_ptr, 3); + const [ptr, len, _cap] = rocStrWords; + + const SEAMLESS_SLICE_BIT = 1 << 31; + const length = len & (~SEAMLESS_SLICE_BIT); + + stringBytes = new Uint8Array(memory.buffer, ptr, length); + } + + const decodedString = repl.textDecoder.decode(stringBytes); + + ROC_PANIC_INFO = { + msg: decodedString, + panic_tag: panic_tag, + }; +} + // Load Wasm code into the browser's virtual machine, so we can run it later. // This operation is async, so we call it before entering any code shared // with the command-line REPL, which is sync. async function js_create_app(wasm_module_bytes) { - const { instance } = await WebAssembly.instantiate(wasm_module_bytes); + const { instance } = await WebAssembly.instantiate(wasm_module_bytes, { + env: { + send_panic_msg_to_js: send_panic_msg_to_js, + } + }); + // Keep the instance alive so we can run it later from shared REPL code repl.app = instance; } @@ -180,13 +220,41 @@ function js_run_app() { // Run the user code, and remember the result address // We'll pass it to Rust in the next callback - repl.result_addr = wrapper(); + try { + repl.result_addr = wrapper(); + } catch (e) { + // an exception could be that roc_panic was invoked, + // or some other crash (likely a compiler bug) + if (ROC_PANIC_INFO === null) { + throw e; + } else { + // when roc_panic set an error message, display it + const { msg, panic_tag } = ROC_PANIC_INFO; + ROC_PANIC_INFO = null; + + console.error(format_roc_panic_message(msg, panic_tag)); + } + } // Tell Rust how much space to reserve for its copy of the app's memory buffer. // We couldn't know that size until we actually ran the app. return memory.buffer.byteLength; } +function format_roc_panic_message(msg, panic_tag) { + switch (panic_tag) { + case 0: { + return `Roc failed with message: "${msg}"`; + } + case 1: { + return `User crash with message: "${msg}"`; + } + default: { + return `Got an invalid panic tag: "${panic_tag}"`; + } + } +} + // After Rust has allocated space for the app's memory buffer, // we copy it, and return the result address too function js_get_result_and_memory(buffer_alloc_addr) { @@ -233,6 +301,21 @@ function updateHistoryEntry(index, ok, outputText) { const historyItem = repl.elemHistory.children[index]; historyItem.appendChild(outputElem); - // Scroll the page to the bottom so you can see the most recent output. - window.scrollTo(0, document.body.scrollHeight); + if (isHomepage) { + // Scroll the input element into view so you can see the most recent output. + // Only do this if it's currently out of view though! + const bounds = repl.elemSourceInput.getBoundingClientRect(); + const isInView = + bounds.top >= 0 && + bounds.left >= 0 && + bounds.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + bounds.right <= (window.innerWidth || document.documentElement.clientWidth); + + if (!isInView) { + repl.elemSourceInput.scrollIntoView({ behavior: "instant", block: "end", inline: "nearest" }); + } + } else { + // Scroll the page to the bottom so you can see the most recent output. + window.scrollTo(0, document.body.scrollHeight); + } } diff --git a/www/wip_new_website/static/site.css b/www/wip_new_website/static/site.css index 75ae1885e9d..8420df42f11 100644 --- a/www/wip_new_website/static/site.css +++ b/www/wip_new_website/static/site.css @@ -4,11 +4,12 @@ --gray: #717171; --orange: #bf5000; --green: #0b8400; - --cyan: #067c94; + --light-cyan: #8af4e6; + --dark-cyan: #4eefd9; --blue: #05006d; --violet: #7c38f5; --violet-bg: #ece2fd; - --magenta: #a20031; + --magenta: #ff32cf; --primary-1: #9b6bf2; --primary-2: #7c38f5; @@ -16,6 +17,7 @@ --link-color: var(--primary-2); --code-link-color: var(--primary-2); --text-color: #000; + --heading-color: #333; --text-hover-color: var(--primary-2); --body-bg-color: #ffffff; --border-color: #717171; @@ -31,6 +33,7 @@ --body-max-width: 1024px; --top-bar-height: 50px; --homepage-intro-box-width: 328px; + --dark-code-bg: #202746; } html { @@ -63,6 +66,17 @@ footer { margin: 0 auto; } +/* Used for e.g. displaying the instruction "Click" on desktop and "Touch" on mobile. + * When we think we're on mobile (based on max-width), we can switch the instruction. +*/ +.desktop { + display: inline; +} + +.mobile { + display: none; +} + section p:last-child { margin-bottom: 0; } @@ -88,24 +102,22 @@ h1, h2, h3, h4 { - font-family: "Permanent Marker"; - padding-left: 12px; + font-weight: bold; } h1 { - color: var(--primary-1); font-size: 5rem; - text-shadow: 1px 1px 1px #010101; } h2 { - color: var(--primary-1); font-size: 3rem; - text-shadow: 1px 1px 1px #010101; + border-bottom: 4px solid var(--dark-cyan); + margin: 36px 0; + padding: 36px 0; + color: var(--heading-color); } h3 { - color: var(--cyan); font-size: 1.5rem; } @@ -202,6 +214,66 @@ pre>samp { display: block; } +#homepage-repl-container { + display: flex; + flex-direction: row-reverse; +} + +#homepage-repl-container #repl-description { + padding: 0 36px; + flex: 1; +} + +#homepage-repl-container #repl-description p:first-of-type { + margin-top: 0; +} + +#homepage-repl-container #repl-description a { + color: inherit; + text-decoration: underline; +} + +#homepage-repl-container #repl-description a:hover { + color: var(--primary-1); +} + +#homepage-repl-container #repl { + flex: 1; + border: 2px solid #444; + box-shadow: 4px 4px var(--dark-cyan); + font-size: 18px; +} + +#homepage-repl-container #repl, #homepage-repl-container #repl code { + color: white; + background-color: var(--dark-code-bg); +} + +#homepage-repl-container #source-input { + margin-bottom: 0; + font-size: 18px; + height: 55px; +} + +#homepage-repl-container #source-input-wrapper { + margin-top: 6px; +} + +#homepage-repl-container p { + position: relative; /* Needed for the repl arrow's position: absolute */ +} + +#homepage-repl-container #repl-arrow { + cursor: default; + font-weight: bold; + color: var(--primary-1); + font-size: 60px; + position: absolute; + top: 0px; + left: -64px; + text-shadow: 1px 1px 1px #444; +} + .repl-prompt:before { /* Add this using CSS so it isn't selectable, which would be annoying when trying to copy/paste! */ color: var(--repl-prompt); @@ -248,7 +320,7 @@ td:last-child { p, aside, li { - font-size: var(--font-size-normal); + font-size: 18px; line-height: 1.85rem; } @@ -261,11 +333,13 @@ li { #homepage-h1 { color: #222; - font-family: "Merriweather"; text-shadow: none; + font-family: inherit; font-size: 64px; padding: 0; padding-top: 60px; + position: relative; + left: -5px; } #homepage-logo { @@ -279,22 +353,22 @@ li { #first-code-sample { width: var(--homepage-intro-box-width); margin-top: 60px; + margin-bottom: 70px; line-height: 1.85em; color: #fcf9fd; + box-shadow: 4px 4px 0px var(--dark-cyan); } #first-code-sample .kw, -#first-code-sample .punctuation { +#first-code-sample .punctuation, +.interactive-example .kw, +.interactive-example .punctuation { color: #9c7cea; } #first-code-sample, #first-code-sample .code-snippet { - background-color: #202746; -} - -#first-code-sample .string { - color: #1bd6bd; + background-color: var(--dark-code-bg); } #homepage-tagline { @@ -303,6 +377,14 @@ li { /* Mobile-friendly screen width */ @media only screen and (max-width: 1024px) { + /* Used for e.g. displaying the instruction "Click" on desktop and "Touch" on mobile. */ + .desktop { + display: none; + } + + .mobile { + display: inline; + } p, aside, @@ -326,6 +408,10 @@ li { white-space: normal; } + .nobreak-on-mobile { + white-space: nowrap; + } + /* Homepage */ #homepage-intro-box { @@ -547,13 +633,14 @@ li { --primary-1: #9c7cea; --primary-2: #1bd6bd; - --text-color: #eaeaea; + --text-color: #ccc; --body-bg-color: #0e0e0f; --border-color: var(--gray); --code-color: #eeeeee; --logo-solid: #8f8f8f; --faded-color: #bbbbbb; --gray: #6e6e6e; + --heading-color: #eee; } #homepage-h1 { @@ -582,12 +669,16 @@ li { td { border-color: var(--gray); } + + #first-code-sample, #homepage-repl-container #repl { + border: 1px solid #ddd; + } } /* Comments `#` and Documentation comments `##` */ samp .comment, code .comment { - color: var(--green); + color: #ccc; } /* Number, String, Tag literals */ @@ -603,7 +694,7 @@ samp .constant, code .constant, samp .literal, code .literal { - color: var(--cyan); + color: var(--dark-cyan); } /* Keywords and punctuation */ @@ -696,8 +787,9 @@ code .dim { display: flex; flex-direction: row; justify-content: space-between; - gap: 60px; + gap: 48px; width: 100%; + margin-bottom: 48px; } .home-goals-column { @@ -712,24 +804,25 @@ code .dim { display: flex; flex-direction: column; padding: 20px; - border: 2px solid var(--primary-2); - box-shadow: 4px 4px var(--primary-2) + border: 4px solid var(--light-cyan); + box-shadow: 4px 4px #777; } .home-goals-content h3 { font-family: inherit; text-transform: lowercase; - color: var(--primary-2); padding-bottom: 12px; padding-top: 2px; + font-style: italic; + text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; + letter-spacing: 1px; + word-spacing: 3px; + margin: 0; } -.home-goals-content p { - font-size: 18px; -} - -.home-goals-content>h3 { - margin: 0; +.home-goals-content a { + color: inherit; + text-decoration: underline; } /* Wider screens */ @@ -755,7 +848,73 @@ code .dim { font-size: 32px; } +.home-goals-title a { + color: var(--light-cyan); + text-decoration: none; +} + +.home-goals-title a:hover { + color: black; + text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; +} + +.home-goals-learn-more a:hover { + text-decoration: none; +} + .home-goals-description { line-height: 1.5; margin-bottom: 2em; } + + +/* Interactive example on homepage */ + +.interactive-example { + box-shadow: 4px 4px var(--dark-cyan); + font-size: 18px; +} + +.interactive-example, .interactive-example samp { + background-color: #202746; + color: white; +} + +.interactive-example samp { + position: relative; + display: block; + width: 100%; + height: 580px; + padding-right: 300px; + cursor: default; +} + +.interactive-example label:hover, +.interactive-radio:checked+label { + background-color: #000; + cursor: pointer; +} + +.interactive-desc { + display: none; + background-color: #ede6ff; + border: 1px solid black; + color: black; + padding: 0 16px; + margin-top: 9px; + cursor: text; +} + +.interactive-radio { + display: none; +} + +.interactive-radio:checked+label+.interactive-desc { + display: block; + position: absolute; + top: 0; + right: 300px; + width: 300px; + white-space: normal; + font-family: -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial; +}