svg.escobar.life

A simple SVG markup editor for the web

Commit
0addc40ecad5fe97c869900f7c1b798be2eddd29
Author
Gark Garcia <37553739+GarkGarcia@users.noreply.github.com>
Date

Initial commit.

Diffstat

14 files changed, 549 insertions, 0 deletions

Status File Name N° Changes Insertions Deletions
Added .gitignore 3 3 0
Added assets/download.svg 6 6 0
Added assets/error.svg 5 5 0
Added assets/moon.svg 5 5 0
Added assets/sun.svg 5 5 0
Added assets/upload.svg 6 6 0
Added build/build.json 8 8 0
Added build/build.py 23 23 0
Added build/template.html 15 15 0
Added elm.json 29 29 0
Added src/Main.elm 63 63 0
Added src/Types.elm 17 17 0
Added src/View.elm 109 109 0
Added styles.css 255 255 0
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+elm-stuff
+build/output+
\ No newline at end of file
diff --git a/assets/download.svg b/assets/download.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Icon by Smashicons (https://www.flaticon.com/authors/smashicons). Download at https://www.flaticon.com/packs/essential-set-2 -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 59 59">
+  <path d="M20.187 28.313c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l9.979 9.979c.186.189.44.294.706.294.007 0 .014-.004.021-.004.007 0 .013.004.021.004.333 0 .613-.173.795-.423l9.891-9.891c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0L30.5 36.544V1c0-.553-.447-1-1-1s-1 .447-1 1v35.628l-8.313-8.315z"/>
+  <path d="M36.5 16c-.553 0-1 .447-1 1s.447 1 1 1h13v39h-40V18h13c.553 0 1-.447 1-1s-.447-1-1-1h-15v43h44V16h-15z"/>
+</svg>
diff --git a/assets/error.svg b/assets/error.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Icon by Dave Gandy (https://www.flaticon.com/authors/dave-gandy). Download at https://www.flaticon.com/free-icon/exclamation_25652 -->
+<svg xmlns="http://www.w3.org/2000/svg" version="1" width="402" height="402" viewBox="0 0 402 402">
+  <path d="M238 301h-74c-4 0-9 2-12 6-4 4-6 8-6 13v64c0 5 2 9 6 13 3 3 8 5 12 5h74c4 0 9-2 12-5 4-4 6-8 6-13v-64c0-5-2-9-6-13-3-4-8-6-12-6zM259 5c-3-3-7-5-12-5h-92c-5 0-9 2-12 5-4 4-6 8-5 13l8 220c0 4 2 9 5 12 4 4 9 6 13 6h74c4 0 9-2 13-6 3-3 5-8 5-12l8-220c1-5-1-9-5-13z"/>
+</svg>
diff --git a/assets/moon.svg b/assets/moon.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Icon by Smashicons (https://www.flaticon.com/authors/smashicons). Download at https://www.flaticon.com/packs/weather-set -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
+  <path d="M25.068,48.889C15.895,48.889 7.051,43.829 2.672,35.085C-3.373,23.008 1.164,8.467 13.003,1.979L15.064,0.85L14.449,3.118C12.97,8.577 13.55,14.368 16.082,19.424C18.832,24.917 23.558,29.011 29.387,30.95C35.218,32.889 41.452,32.442 46.946,29.692C47.196,29.567 47.438,29.434 47.68,29.301L49.741,28.171L49.156,30.423C47.293,37.296 42.579,43.062 36.223,46.245C32.639,48.039 28.825,48.888 25.068,48.889ZM12.002,4.936C2.589,11.364 -0.754,23.773 4.462,34.189C10.14,45.529 23.984,50.134 35.326,44.457C40.48,41.875 44.462,37.445 46.507,32.1C40.875,34.527 34.625,34.802 28.755,32.848C22.418,30.74 17.282,26.291 14.292,20.32C11.899,15.541 11.11,10.16 12.002,4.936Z"/>
+</svg>
diff --git a/assets/sun.svg b/assets/sun.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60">
+  <path d="M30 14c-8.822 0-16 7.178-16 16s7.178 16 16 16 16-7.178 16-16-7.178-16-16-16zm0 30c-7.72 0-14-6.28-14-14s6.28-14 14-14 14 6.28 14 14-6.28 14-14 14zM30 8c.552 0 1-.448 1-1V1c0-.552-.448-1-1-1s-1 .448-1 1v6c0 .552.448 1 1 1zM30 52c-.552 0-1 .448-1 1v6c0 .552.448 1 1 1s1-.448 1-1v-6c0-.552-.448-1-1-1zM59 29h-6c-.552 0-1 .448-1 1s.448 1 1 1h6c.552 0 1-.448 1-1s-.448-1-1-1zM8 30c0-.552-.448-1-1-1H1c-.552 0-1 .448-1 1s.448 1 1 1h6c.552 0 1-.448 1-1zM46.264 14.736c.256 0 .512-.098.707-.293l5.736-5.736c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0l-5.736 5.736c-.391.391-.391 1.023 0 1.414.195.196.451.293.707.293zM13.029 45.557l-5.736 5.736c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293.256 0 .512-.098.707-.293l5.736-5.736c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0zM46.971 45.557c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l5.736 5.736c.195.195.451.293.707.293.256 0 .512-.098.707-.293.391-.391.391-1.023 0-1.414l-5.736-5.736zM13.029 14.443c.195.195.451.293.707.293.256 0 .512-.098.707-.293.391-.391.391-1.023 0-1.414L8.707 7.293c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l5.736 5.736z"/>
+</svg>+
\ No newline at end of file
diff --git a/assets/upload.svg b/assets/upload.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Icon by Smashicons (https://www.flaticon.com/authors/smashicons). Download at https://www.flaticon.com/packs/essential-set-2 -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 59 59">
+  <path d="M19.479 12.019c.256 0 .512-.098.707-.293l8.313-8.313v35.586c0 .553.447 1 1 1s1-.447 1-1V3.413l8.272 8.272c.391.391 1.023.391 1.414 0s.391-1.023 0-1.414L30.207.293C30.115.2 30.004.127 29.88.076c-.244-.101-.519-.101-.764 0-.123.051-.234.125-.326.217L18.772 10.312c-.391.391-.391 1.023 0 1.414.195.196.451.293.707.293z"/>
+  <path d="M36.499 15.999c-.553 0-1 .447-1 1s.447 1 1 1h13v39h-40v-39h13c.553 0 1-.447 1-1s-.447-1-1-1h-15v43h44v-43h-15z"/>
+</svg>
diff --git a/build/build.json b/build/build.json
@@ -0,0 +1,7 @@
+{
+    "srcs": ["../src/Main.elm", "../src/View.elm", "../src/Types.elm"],
+    "template": "template.html",
+    "stylesheet": "../styles.css",
+    "charset": "UTF-8",
+    "icon": ""
+}+
\ No newline at end of file
diff --git a/build/build.py b/build/build.py
@@ -0,0 +1,22 @@
+import json, os
+
+print("Loading build configs...")
+
+with open("build.json", "r", encoding="utf8") as build_config:
+    config = json.loads(build_config.read())
+
+    print("Compiling source files...")
+    srcs = ["'" + s + "'" for s in config["srcs"]]
+    os.system("elm make {} --optimize --output='output/script.js'".format(" ".join(srcs)))
+
+    print("Compressing compiled ouput...")
+    os.system("minify 'output/script.js' --out-file 'output/script.js' --mangle")
+
+    with open(config["template"], "r", encoding="utf8") as template_file:
+        template = template_file.read()
+
+        with open("output/index.html", "w", encoding="utf8") as output:
+            print("Compiling index.html...")
+            output.write(template.format(**config))
+
+print("\nAll done!")+
\ No newline at end of file
diff --git a/build/template.html b/build/template.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+	<head>
+	  <meta charset="{charset}">
+	  <meta name="viewport" content="width=device-width, initial-scale=1">
+	  <link rel="stylesheet" href="{stylesheet}">
+	  <link rel="icon" type="image/svg" href="{icon}">
+	  <script src="script.js"></script>
+	</head>
+
+	<body>
+		<script>Elm.Main.init()</script>
+	</body>
+</html>+
\ No newline at end of file
diff --git a/elm.json b/elm.json
@@ -0,0 +1,28 @@
+{
+    "type": "application",
+    "source-directories": [
+        "src"
+    ],
+    "elm-version": "0.19.0",
+    "dependencies": {
+        "direct": {
+            "elm/browser": "1.0.1",
+            "elm/core": "1.0.2",
+            "elm/file": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/json": "1.1.3",
+            "elm/regex": "1.0.0",
+            "elm/svg": "1.0.1"
+        },
+        "indirect": {
+            "elm/bytes": "1.0.8",
+            "elm/time": "1.0.0",
+            "elm/url": "1.0.0",
+            "elm/virtual-dom": "1.0.2"
+        }
+    },
+    "test-dependencies": {
+        "direct": {},
+        "indirect": {}
+    }
+}+
\ No newline at end of file
diff --git a/src/Main.elm b/src/Main.elm
@@ -0,0 +1,63 @@
+module Main exposing (main)
+
+import View exposing (view)
+import Types exposing (..)
+
+import Html exposing (Html)
+import Browser exposing (document)
+import File exposing (File)
+import File.Download as Download
+import File.Select as Select
+import Regex exposing (never, fromString)
+import Task
+
+
+main : Program () Model Msg
+main =
+  document
+    { init = \_ -> (init, Cmd.none)
+    , update = update
+    , view = view
+    , subscriptions = subscriptions
+    }
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+    case msg of
+        Update image ->
+            ({ model | image = image }, Cmd.none)
+
+        Validation val ->
+            ({ model | isValid = val }, Cmd.none)
+
+        DarkModeToggle ->
+            ({ model | darkMode = not model.darkMode }, Cmd.none)
+
+        Download ->
+            (model, Download.string "image.svg" "image/svg+xml" model.image)
+
+        Upload upl ->
+            (model, upload upl)
+
+upload : Upload -> Cmd Msg
+upload upl =
+    case upl of
+        Requested  ->
+            Select.file ["image/svg+xml"] (\file -> Upload (Selected file))
+
+        Selected file ->
+            Task.perform Update (File.toString file)
+
+
+subscriptions : Model -> Sub Msg
+subscriptions model =
+    Sub.none
+
+init : Model
+init =
+    { image = "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1\" viewBox=\"0 0 100 100\">\n  <circle cx=\"50\" cy=\"50\" r=\"50\" fill=\"rgb(67, 54, 76)\" />\n</svg>"
+    , isValid = True
+    , darkMode = False
+    , uriEncoder = Maybe.withDefault Regex.never <| Regex.fromString "!|#|\\$|%|&|'|\\(|\\)|\\*|\\+|,|\\/|:|;|=|\\?|@|\\[|\\]"
+    }
diff --git a/src/Types.elm b/src/Types.elm
@@ -0,0 +1,16 @@
+module Types exposing (..)
+
+import File exposing (File)
+import Regex exposing (Regex)
+
+type alias Model =
+    { image : String, isValid : Bool, darkMode : Bool, uriEncoder : Regex }
+
+type Upload = Requested | Selected File
+
+type Msg
+    = Update String
+    | Validation Bool
+    | DarkModeToggle
+    | Download
+    | Upload Upload+
\ No newline at end of file
diff --git a/src/View.elm b/src/View.elm
@@ -0,0 +1,108 @@
+module View exposing (view)
+
+import Types exposing (..)
+import Html exposing (Html, div, img, button, a, textarea, text)
+import Html.Events exposing (onClick, onInput, on)
+import Html.Attributes exposing (id, class, src, value, spellcheck, placeholder)
+import Browser exposing (Document)
+import Svg exposing (svg, path)
+import Svg.Attributes exposing (d, viewBox, fill)
+import Json.Decode
+import Regex exposing (Regex, Match, replace)
+
+
+view : Model -> Document Msg
+view model =
+    { title = "RawSvg"
+    , body =
+        [ div [ id "header" ] []
+        , div [ id "editor", class (if model.isValid then "" else "error") ] (editor model)
+        ]
+    }
+
+display : Model -> Html Msg
+display model =
+    div [ id "display", class (if model.darkMode then "dark" else "") ]
+        [ img
+            [ src (uri model)
+            , if model.isValid 
+                then on "error" (Json.Decode.succeed (Validation False))
+                else on "load"  (Json.Decode.succeed (Validation True))
+            ] []
+        , div [ id "options" ]
+            [ button [ onClick Download ] [ downloadIcon ]
+            , button [ onClick (Upload Requested) ] [ uploadIcon ]
+            , button [ id "dark-toggle", onClick DarkModeToggle ] [ darkModeIcon model ]
+            ]
+        ]
+
+editor : Model -> List (Html Msg)
+editor model =
+    [ display model
+    , errorIcon
+    , text model
+    ]
+
+text : Model -> Html Msg
+text model =
+    textarea 
+        [ value model.image
+        , placeholder "<svg ...> ... </svg>"
+        , spellcheck False
+        , onInput (\input -> Update input)
+        ] []
+
+uri : Model -> String
+uri model =
+    "data:image/svg+xml;utf8," ++ (replace model.uriEncoder percentEncode model.image)
+
+percentEncode : Match -> String
+percentEncode m =
+    case m.match of
+        "!" -> "%21"
+        "#" -> "%23"
+        "$" -> "%24"
+        "%" -> "%25"
+        "&" -> "%26"
+        "'" -> "%27"
+        "(" -> "%28"
+        ")" -> "%29"
+        "*" -> "%2A"
+        "+" -> "%2B"
+        "," -> "%2C"
+        "/" -> "%2F"
+        ":" -> "%3A"
+        ";" -> "%3B"
+        "=" -> "%3D"
+        "?" -> "%3F"
+        "@" -> "%40"
+        "[" -> "%5B"
+        "]" -> "%5D"
+        str -> str
+
+downloadIcon : Html Msg
+downloadIcon =
+    svg [ viewBox "0 0 59 59" ]
+        [ path [ d "M20.187 28.313c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l9.979 9.979c.186.189.44.294.706.294.007 0 .014-.004.021-.004.007 0 .013.004.021.004.333 0 .613-.173.795-.423l9.891-9.891c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0L30.5 36.544V1c0-.553-.447-1-1-1s-1 .447-1 1v35.628l-8.313-8.315z" ] []
+        , path [ d "M36.5 16c-.553 0-1 .447-1 1s.447 1 1 1h13v39h-40V18h13c.553 0 1-.447 1-1s-.447-1-1-1h-15v43h44V16h-15z" ] []
+        ]
+
+uploadIcon : Html Msg
+uploadIcon =
+    svg [ viewBox "0 0 59 59" ]
+        [ path [ d "M19.479 12.019c.256 0 .512-.098.707-.293l8.313-8.313v35.586c0 .553.447 1 1 1s1-.447 1-1V3.413l8.272 8.272c.391.391 1.023.391 1.414 0s.391-1.023 0-1.414L30.207.293C30.115.2 30.004.127 29.88.076c-.244-.101-.519-.101-.764 0-.123.051-.234.125-.326.217L18.772 10.312c-.391.391-.391 1.023 0 1.414.195.196.451.293.707.293z" ] []
+        , path [ d "M36.499 15.999c-.553 0-1 .447-1 1s.447 1 1 1h13v39h-40v-39h13c.553 0 1-.447 1-1s-.447-1-1-1h-15v43h44v-43h-15z" ] []
+        ]
+
+darkModeIcon : Model -> Html Msg
+darkModeIcon model =
+    if model.darkMode
+        then svg [ id "light-toggle", viewBox "0 0 60 60" ]
+            [ path [ d "M30 14c-8.822 0-16 7.178-16 16s7.178 16 16 16 16-7.178 16-16-7.178-16-16-16zm0 30c-7.72 0-14-6.28-14-14s6.28-14 14-14 14 6.28 14 14-6.28 14-14 14zM30 8c.552 0 1-.448 1-1V1c0-.552-.448-1-1-1s-1 .448-1 1v6c0 .552.448 1 1 1zM30 52c-.552 0-1 .448-1 1v6c0 .552.448 1 1 1s1-.448 1-1v-6c0-.552-.448-1-1-1zM59 29h-6c-.552 0-1 .448-1 1s.448 1 1 1h6c.552 0 1-.448 1-1s-.448-1-1-1zM8 30c0-.552-.448-1-1-1H1c-.552 0-1 .448-1 1s.448 1 1 1h6c.552 0 1-.448 1-1zM46.264 14.736c.256 0 .512-.098.707-.293l5.736-5.736c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0l-5.736 5.736c-.391.391-.391 1.023 0 1.414.195.196.451.293.707.293zM13.029 45.557l-5.736 5.736c-.391.391-.391 1.023 0 1.414.195.195.451.293.707.293.256 0 .512-.098.707-.293l5.736-5.736c.391-.391.391-1.023 0-1.414s-1.023-.391-1.414 0zM46.971 45.557c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l5.736 5.736c.195.195.451.293.707.293.256 0 .512-.098.707-.293.391-.391.391-1.023 0-1.414l-5.736-5.736zM13.029 14.443c.195.195.451.293.707.293.256 0 .512-.098.707-.293.391-.391.391-1.023 0-1.414L8.707 7.293c-.391-.391-1.023-.391-1.414 0s-.391 1.023 0 1.414l5.736 5.736z" ] [] ]
+        else svg [ id "light-toggle", viewBox "0 0 50 50" ]
+            [ path [ d "M25.068,48.889C15.895,48.889 7.051,43.829 2.672,35.085C-3.373,23.008 1.164,8.467 13.003,1.979L15.064,0.85L14.449,3.118C12.97,8.577 13.55,14.368 16.082,19.424C18.832,24.917 23.558,29.011 29.387,30.95C35.218,32.889 41.452,32.442 46.946,29.692C47.196,29.567 47.438,29.434 47.68,29.301L49.741,28.171L49.156,30.423C47.293,37.296 42.579,43.062 36.223,46.245C32.639,48.039 28.825,48.888 25.068,48.889ZM12.002,4.936C2.589,11.364 -0.754,23.773 4.462,34.189C10.14,45.529 23.984,50.134 35.326,44.457C40.48,41.875 44.462,37.445 46.507,32.1C40.875,34.527 34.625,34.802 28.755,32.848C22.418,30.74 17.282,26.291 14.292,20.32C11.899,15.541 11.11,10.16 12.002,4.936Z" ] [] ]
+
+errorIcon : Html Msg
+errorIcon =
+    svg [ id "error", viewBox "0 0 402 402" ]
+        [ path [ d "M238 301h-74c-4 0-9 2-12 6-4 4-6 8-6 13v64c0 5 2 9 6 13 3 3 8 5 12 5h74c4 0 9-2 12-5 4-4 6-8 6-13v-64c0-5-2-9-6-13-3-4-8-6-12-6zM259 5c-3-3-7-5-12-5h-92c-5 0-9 2-12 5-4 4-6 8-5 13l8 220c0 4 2 9 5 12 4 4 9 6 13 6h74c4 0 9-2 13-6 3-3 5-8 5-12l8-220c1-5-1-9-5-13z" ] [] ]+
\ No newline at end of file
diff --git a/styles.css b/styles.css
@@ -0,0 +1,254 @@
+@import url('https://fonts.googleapis.com/css?family=B612+Mono&display=swap');
+
+:root {
+    align-self: center;
+    align-content: center;
+    align-items: center;
+    text-align: center;
+    
+    cursor: default;
+    -webkit-user-select: none; /* Safari */        
+    -moz-user-select: none; /* Firefox */
+    -ms-user-select: none; /* IE + Edge */
+    user-select: none; /* Standard */
+
+    scrollbar-width: none; /* Firefox */
+    -ms-overflow-style: none; /* IE + Edge */
+
+    --dark: #43364c;
+    --mid: #abbfc5;
+    --light: #e0ebe0;
+    --highlight: #6387c9;
+}
+
+@media (hover: hover) {
+    :root {
+        --large: 10vmax;
+        --small: 2.5vmax;
+        --tiny: 1vmax;
+    }
+}
+
+@media (hover: none) {
+    :root {
+        --large: 15vmax;
+        --small: 3vmax;
+        --tiny: 2vmax;
+    }
+}
+
+:root::-webkit-scrollbar {
+    width: 0px; /*Chrome + Safari + Opera*/
+}
+
+textarea::selection {
+    background: var(--dark);
+    color: var(--mid);
+}
+
+textarea::-moz-selection {
+    background: var(--dark);
+    color: var(--mid);
+}
+
+textarea::placeholder {
+    color: var(--light);
+    opacity: 1;
+}
+
+svg {
+    fill-rule: evenodd;
+    clip-rule: evenodd;
+}
+
+body {
+    width: 100%;
+    height: 100%;
+
+    margin: 0;
+    padding: 0;
+
+    background: var(--dark);
+}
+
+body > div#header {
+    width: 100%;
+    height: var(--large);
+
+    background: var(--mid);
+}
+
+body > div#editor {
+    display: grid;
+
+    grid-gap: calc(1.5 * var(--tiny));
+    margin: calc(1.5 * var(--small)) calc(3 * var(--small));
+    padding: calc(1.5 * var(--tiny));
+
+    background: var(--highlight);
+    box-shadow: 0 0 var(--small) 1vmax #00000040;
+    
+    border-radius: calc(2 * var(--tiny));
+}
+
+body > div#editor.error {
+    --mid: #c2858f;
+    --light: #eec6df;
+    --highlight: #c73a4d;
+}
+
+@media (orientation: landscape) {
+    body > div#editor {
+        grid-template-columns: 1fr 2fr;
+        grid-template-areas: "display text";
+        
+        min-height: 25vw;
+    }
+}
+
+@media (orientation: portrait) {
+    body > div#editor {
+        grid-template-rows: 3fr 1fr;
+        grid-template-areas: "display" "text";
+        
+        height: 70vh;
+    }
+}
+
+body > div#editor, body > div#editor > * {
+    transition: background-color .25s ease, border-color .25s ease;
+}
+
+body > div#editor img {
+    width: 100%;
+    height: 100%;
+}
+
+body > div#editor > div#display {
+    grid-area: display;
+    position: relative;
+
+    background: var(--light);
+    border-radius: var(--tiny);
+
+    box-shadow: inset 0 0 calc(.25 * var(--small)) .5vmax #00000015;
+    transition: background .5s ease, box-shadow .5s ease;
+}
+
+body > div#editor > div#display.dark {
+    background: var(--dark);
+    box-shadow: inset 0 0 calc(.25 * var(--small)) .5vmax #ffffff10;
+}
+
+body > div#editor > div#display > * {
+    position: absolute;
+}
+
+body > div#editor > div#display > img {
+    top: 0;
+    left: 0;
+    
+    width: calc(100% - 4 * var(--tiny));
+    height: calc(100% - 4 * var(--tiny));
+    
+    padding: calc(2 * var(--tiny));
+}
+
+body > div#editor > div#display > div#options {
+    display: grid;
+    grid-gap: var(--tiny);
+    grid-auto-flow: column;
+    
+    bottom: 0;
+    left: 0;
+
+    z-index: 1;
+    padding: var(--tiny);
+    
+    transition: opacity .5s ease;
+}
+
+@media (hover: hover) {
+    body > div#editor > div#display > div#options {
+        grid-gap: var(--tiny);
+        opacity: .75;
+    }
+
+    body > div#editor > div#display > div#options:hover {
+        opacity: 1;
+    }
+}
+
+@media (hover: none) {
+    body > div#editor > div#display > div#options {
+        width: calc(100% - 2 * var(--tiny));
+    }
+}
+
+body > div#editor > div#display > div#options > button {
+    display: flex;
+
+    width: calc(4 * var(--tiny));
+    height: calc(4 * var(--tiny));
+
+    margin: auto;
+
+    background: var(--highlight);
+    border: none;
+    border-radius: calc(2 * var(--tiny));
+
+    box-shadow: 0 0 calc(.25 * var(--small)) .5vmax #00000030;
+    
+    cursor: pointer;
+}
+
+body > div#editor svg {
+    max-width: 90%;
+    max-height: 90%;
+
+    margin: auto;
+
+    fill: var(--light);
+}
+
+body > div#editor > svg#error, body > div#editor.error > div#display {
+    display: none;
+}
+
+body > div#editor.error > svg#error {
+    display: block;
+}
+
+body > div#editor > textarea {
+    grid-area: text;
+    resize: none;
+    white-space: pre;
+
+    width: 100%;
+    height: 100%;
+
+    margin: 0;
+
+    background: transparent;
+    border: none;
+    
+
+    font-family: 'B612 Mono', monospace;
+    color: var(--light);
+    font-style: italic;
+
+    scrollbar-width: none; /*For Firefox*/
+    -ms-overflow-style: none; /*For IE and Edge*/
+}
+
+@media (hover: hover) {
+    body > div#editor > textarea {
+        font-size: 1.2vmax;
+    }
+}
+
+@media (hover: none) {
+    body > div#editor > textarea {
+        font-size: 2.25vmax;
+    }
+}+
\ No newline at end of file