commit 899087ed49d28f9c7929629da4f0ee87f9c30387 Author: Christian Lawson-Perfect Date: Sat Feb 8 14:48:40 2025 +0000 first commit This is the first commit. With stuff on several lines. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d70f946 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.make.* +dist/code-editor.mjs +dist/think-editor.js +elm-stuff +jj/status +log +error.txt diff --git a/.watchmakerc b/.watchmakerc new file mode 100644 index 0000000..285f521 --- /dev/null +++ b/.watchmakerc @@ -0,0 +1,2 @@ +extensions: + - .elm \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..66ada12 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +DIRNAME=$(notdir $(CURDIR)) + +PROD_DIR=/srv/think.somethingorotherwhatever.com/thinks/static/thinks + +ELMS=$(wildcard src/*.elm) + +deploy: $(PROD_DIR)/think-editor.js $(PROD_DIR)/load-think-editor.mjs $(PROD_DIR)/think-editor.css + +build: dist/think-editor.js + +dist/think-editor.js: src/App.elm $(ELMS) + -elm make $< --output=$@ 2> error.txt + @cat error.txt + +$(PROD_DIR)/%: dist/% + cp $< $@ + +upload: app.js index.html style.css + rsync -avz . clpland:~/domains/somethingorotherwhatever.com/html/$(DIRNAME) + @echo "Uploaded to https://somethingorotherwhatever.com/$(DIRNAME)" diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..9c02b6f --- /dev/null +++ b/README.txt @@ -0,0 +1 @@ +The Elm app for the think editor. \ No newline at end of file diff --git a/dist/load-think-editor.mjs b/dist/load-think-editor.mjs new file mode 100644 index 0000000..482d2e1 --- /dev/null +++ b/dist/load-think-editor.mjs @@ -0,0 +1,26 @@ +import './code-editor.mjs'; + +export default async function init_app() { + const flags = JSON.parse(document.getElementById('think-editor-data').textContent); + const packages = await (await fetch('https://elm-package-list.think.somethingorotherwhatever.com/elm-packages.json')).json(); + flags.elm_packages = packages; + flags.csrf_token = document.getElementById('csrftoken')?.textContent || ''; + const app = Elm.App.init({node: document.body, flags}); + + app.ports.reload_preview.subscribe(() => { + console.log('reload preview'); + const iframe = document.getElementById('preview-frame'); + if(iframe) { + const src = iframe.src; + iframe.src = ""; + setTimeout(() => { + iframe.src = src; + },10); + } + }) + + app.ports.show_modal.subscribe(id => { + console.log(id); + document.getElementById(id).showModal() + }) +} \ No newline at end of file diff --git a/dist/think-editor.css b/dist/think-editor.css new file mode 100644 index 0000000..7939736 --- /dev/null +++ b/dist/think-editor.css @@ -0,0 +1,243 @@ +:root { + --spacing: 1em; + --half-spacing: calc(0.5 * var(--spacing)); + --double-spacing: calc(2 * var(--spacing)); + + --editor-size: 50%; +} + +* { + box-sizing: border-box; +} + +body { + color-scheme: light dark; + font-family: sans-serif; + + display: grid; + grid-template-rows: auto 1fr; + min-height: 100vh; + margin: 0; + padding: var(--half-spacing); + + & > header { + padding: var(--spacing); + & h1 { + margin: 0; + } + } + +} + +@media (prefers-color-scheme: dark) { + body { + background: black; + color: white; + } +} + +header { + & #think-controls { + display: flex; + gap: var(--spacing); + margin: 0; + } +} + +#editor-size-input + output { + width: 5em; +} + +.file-path { + font-family: monospace; +} + + +.think-editor { + display: grid; + gap: var(--spacing); + height: 100%; + overflow: hidden; + --col-1-width: auto; + grid-template: + "nav editor preview" min-content + "log editor preview" 1fr + / var(--col-1-width) var(--editor-size) var(--preview-size) + ; + + &:has(#main-nav[open], #log[open]) { + --col-1-width: 20em; + } + + & > #main-nav { + grid-area: nav; + } + & > #log { + grid-area: log; + width: 100%; + overflow: auto; + } + + & .dragging { + background: red; + } + + & summary { + background: #eee; + } + + & > #main-nav > nav { + display: flex; + flex-direction: column; + gap: var(--spacing); + + & #file-tree { + margin: 0; + overflow: auto; + flex-grow: 1; + + & > li { + margin-top: var(--half-spacing); + + & > a { + text-decoration: none; + } + } + + & .dir { + font-weight: bold; + } + + & .dir + .file { + margin-top: var(--spacing); + } + } + + & form { + display: grid; + grid-template-columns: 1fr 6em; + align-content: start; + align-items: start; + } + + & #make-log { + & > pre { + max-width: 20em; + overflow: auto; + } + } + } + + & #editor { + overflow-x: hidden; + flex-grow: 1; + flex-basis: var(--editor-size); + max-height: 85vh; + grid-area: editor; + + & #editor-controls { + position: sticky; + top: 0; + display: flex; + gap: var(--spacing); + justify-content: space-between; + + & > details { + text-align: right; + + & > summary { + user-select: none; + } + + & button { + margin: var(--half-spacing) 0; + } + } + background: white; + z-index: 1; + } + + & #code-editor { + display: block; + max-width: 50vw; + padding-bottom: 10em; + } + } + + & #preview { + display: flex; + flex-direction: column; + grid-area: preview; + + &[open] { + flex-grow: 1; + flex-shrink: 1; + flex-basis: calc(100% - var(--editor-size)); + } + + & > summary { + text-align: right; + } + + & > iframe { + width: 100%; + height: calc(100% - 3em); + border: none; + } + + &[closed] > iframe { + display: none; + } + } +} + +#file-form { + overflow: auto; + max-width: 100%; +} +@media (max-width: 100ch) { + html { + font-size: min(3vw, 16px); + } + body { + grid-template-columns: calc(100svw - var(--spacing)); + } + .think-editor { + overflow: visible; + grid-template: + "nav" + "log" + "editor" + "preview" + ; + + & > * ~ * { + border-top: medium solid #888; + margin-top: var(--spacing); + } + + & nav { + flex-direction: row; + flex-wrap: wrap; + + & form { + flex-grow: 1; + } + + & #file-tree { + max-height: 7em; + } + } + + & #editor { + overflow: auto; + & #code-editor { + max-width: none; + } + } + + & #preview { + height: 100vh; + } + } +} \ No newline at end of file diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..22847c3 --- /dev/null +++ b/elm.json @@ -0,0 +1,30 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/file": "1.0.5", + "elm/html": "1.0.0", + "elm/http": "2.0.0", + "elm/json": "1.1.3", + "elm/url": "1.0.0", + "elm-community/json-extra": "4.3.0" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/parser": "1.1.0", + "elm/time": "1.0.0", + "elm/virtual-dom": "1.0.3", + "rtfeldman/elm-iso8601-date-strings": "1.1.4" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..fb11fda --- /dev/null +++ b/index.html @@ -0,0 +1,39 @@ + + + + + + Elm app by clp + + + + +

Think editor test page

+

It's loading!

+ + + + + + \ No newline at end of file diff --git a/show-error.mjs b/show-error.mjs new file mode 100644 index 0000000..0c9d53e --- /dev/null +++ b/show-error.mjs @@ -0,0 +1,21 @@ +export default fetch('/error.txt').then(r=>{ + if(r.ok) { + return r.text(); + } else { + throw(''); + } +}).then(text => { + if(!text) { + return false; + } + document.body.innerHTML = ''; + const error_show = document.createElement('pre'); + error_show.setAttribute('id','build-error'); + error_show.style.background = 'black'; + error_show.style.color = 'white'; + error_show.style.padding = '1em'; + error_show.style['font-size'] = '16px'; + error_show.textContent = text; + document.body.appendChild(error_show); + return true; +}).catch(e => false); diff --git a/src/App.elm b/src/App.elm new file mode 100644 index 0000000..06c2e1a --- /dev/null +++ b/src/App.elm @@ -0,0 +1,702 @@ +port module App exposing (..) + +import Browser +import Browser.Navigation +import File exposing (File) +import Html as H exposing (Html) +import Html.Attributes as HA +import Html.Events as HE +import Http exposing (Error(..)) +import Json.Decode as JD +import Json.Decode.Extra exposing (andMap) +import Json.Encode as JE +import Process +import Task +import Url.Builder as UB + +port reload_preview : () -> Cmd msg + +port show_modal : String -> Cmd msg + +delayMsg : Float -> msg -> Cmd msg +delayMsg delay msg = + Task.perform (always msg) (Process.sleep delay) + +main = Browser.document + { init = init + , update = update + , subscriptions = subscriptions + , view = view + } + +type alias FileInfo = + { name : String + , is_dir : Bool + , path : String + } + +type alias ElmPackage = + { name : String + , summary : String + , version : String + , license : String + } + +decode_elm_package = + JD.map4 + ElmPackage + (JD.field "name" JD.string) + (JD.field "summary" JD.string) + (JD.field "version" JD.string) + (JD.field "license" JD.string) + +file_info name = { name = name, path = name, is_dir = False } +dir_info name = { name = name, path = name, is_dir = True } + +file_edit_url f = (UB.relative [] [UB.string "path" f.path]) + +type Log + = NotLoaded + | HttpError Http.Error + | MakeResult String String + | MakeError String + | CommandResult String String + +type alias CommandResult = Result String { stdout : String, stderr : String } + +type alias Model = + { show_preview : Bool + , show_log : Bool + , editor_size : Float + , content_changed : Bool + , log : Log + , command_to_run : String + , selected_package : String + , jj_status : String + , commit_message : String + + , csrf_token : String + , preview_url : String + , slug : String + , files : List FileInfo + , file_path : String + , file_content : String + , is_dir : Bool + , elm_packages : List ElmPackage + } + +init_model : Model +init_model = + { show_preview = True + , show_log = False + , editor_size = 0.5 + , content_changed = False + , log = NotLoaded + , command_to_run = "" + , selected_package = "" + , jj_status = "" + , commit_message = "" + + , csrf_token = "" + , preview_url = "" + , slug = "not-a-real-thing" + , files = [] + , file_path = "Oops!" + , file_content = "The editor has not loaded successfully." + , is_dir = False + , elm_packages = [] + } + +type DropFileAction + = Upload + | Content + +type Msg + = SetFileContent String + | SaveContent String + | FileSaved String (Result Http.Error CommandResult) + | SetLog (Result Http.Error String) + | ReloadLog + | ReloadPreview + | SetEditorSize Float + | TogglePreview Bool + | ToggleLog Bool + | DropFiles DropFileAction (List File) + | NoOp + | UploadFile File String + | FileUploaded File + | SetCommand String + | RunCommand String + | ReceiveCommandResult (Result Http.Error CommandResult) + | SelectPackage String + | ShowCommitModal + | ReceiveJJStatus String + | SetCommitMessage String + | JJCommit + | JJCommitResponse (Result Http.Error CommandResult) + +init : JE.Value -> (Model, Cmd Msg) +init flags = (load_flags flags, reload_log) + +load_flags = + JD.decodeValue + ( JD.succeed + (Model + init_model.show_preview + init_model.show_log + init_model.editor_size + init_model.content_changed + init_model.log + init_model.command_to_run + init_model.selected_package + init_model.jj_status + init_model.commit_message + ) + |> andMap (JD.field "csrf_token" JD.string) + |> andMap (JD.field "preview_url" JD.string) + |> andMap (JD.field "slug" JD.string) + |> andMap (JD.field "files" (JD.list decode_file_info)) + |> andMap (JD.field "file_path" JD.string) + |> andMap (JD.field "file_content" JD.string) + |> andMap (JD.field "is_dir" JD.bool) + |> andMap (JD.oneOf [JD.field "elm_packages" (JD.list decode_elm_package), JD.succeed []]) + ) + >> Result.withDefault init_model + +decode_file_info = + JD.map3 + FileInfo + (JD.field "name" JD.string) + (JD.field "is_dir" JD.bool) + (JD.field "path" JD.string) + +decode_command_response =aq + JD.oneOf + [ JD.field "error" JD.string |> JD.map Err + , JD.map2 (\stdout stderr -> Ok { stdout = stdout, stderr = stderr }) + (JD.field "stdout" JD.string) + (JD.field "stderr" JD.string) + ] + +set_log : Log -> Model -> Model +set_log log model = { model | log = log, show_log = True } + +nocmd model = (model, Cmd.none) + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = case msg of + SetFileContent content -> ({ model | file_content = content, content_changed = True }, delayMsg 1000 (SaveContent content)) + + SaveContent content -> + if content == model.file_content then + ( model + , Http.post + { url = "save-file" + , body = Http.multipartBody + [ Http.stringPart "path" model.file_path + , Http.stringPart "content" model.file_content + , Http.stringPart "csrfmiddlewaretoken" model.csrf_token + ] + , expect = Http.expectJson (FileSaved content) decode_command_response + } + ) + else + (model, Cmd.none) + + FileSaved content response -> case response of + Ok (Ok res) -> ({ model | content_changed = content /= model.file_content, log = MakeResult res.stdout res.stderr }, delayMsg 1 ReloadPreview) + Ok (Err errmsg) -> ({ model | content_changed = content /= model.file_content, log = MakeError errmsg }, delayMsg 1 ReloadPreview) + Err err -> { model | log = HttpError err } |> nocmd + + ReloadLog -> + (model + , reload_log + ) + + SetLog response -> case response of + Ok log -> + { model | log = (MakeResult log "") } |> nocmd + Err errmsg -> model |> set_log (HttpError errmsg) |> nocmd + + ReloadPreview -> (model, reload_preview ()) + + SetEditorSize size -> { model | editor_size = size } |> nocmd + + TogglePreview show -> { model | show_preview = show } |> nocmd + + ToggleLog show -> { model | show_log = show } |> nocmd + + DropFiles action files -> case List.head files of + Just file -> case action of + Content -> (model, Task.perform SetFileContent (File.toString file)) + Upload -> (model, Task.perform (UploadFile file) (File.toString file)) + Nothing -> (model, Cmd.none) + + UploadFile file contents -> + ( model + , Http.post + { url = "save-file" + , body = Http.multipartBody + [ Http.stringPart "path" (File.name file) + , Http.stringPart "content" contents + , Http.stringPart "csrfmiddlewaretoken" model.csrf_token + ] + , expect = Http.expectWhatever + (\r -> case r of + Ok _ -> FileUploaded file + Err _ -> NoOp + ) + } + ) + + FileUploaded file -> + let + name = File.name file + url = file_edit_url { name = name, path = name, is_dir = False } + in + (model, Browser.Navigation.load url) + + SetCommand cmd -> { model | command_to_run = cmd } |> nocmd + + RunCommand cmd -> + (model + , Http.post + { url = "run-command" + , body = Http.multipartBody + [ Http.stringPart "command" cmd + , Http.stringPart "csrfmiddlewaretoken" model.csrf_token + ] + , expect = Http.expectJson ReceiveCommandResult decode_command_response + } + ) + + SelectPackage name -> { model | selected_package = name } |> nocmd + + ReceiveCommandResult response -> case response of + Ok (Ok res) -> model |> set_log (MakeResult res.stdout res.stderr) |> nocmd + Ok (Err errmsg) -> model |> set_log (MakeError errmsg) |> nocmd + Err err -> model |> set_log (HttpError err) |> nocmd + + ShowCommitModal -> (model, Cmd.batch [fetch_jj_status, show_modal "commit-modal"]) + + SetCommitMessage message -> { model | commit_message = message } |> nocmd + + ReceiveJJStatus status -> { model | jj_status = status, commit_message = "" } |> nocmd + + JJCommit -> + ( model + , Http.post + { url = "jj/commit" + , body = Http.multipartBody + [ Http.stringPart "message" model.commit_message + , Http.stringPart "csrfmiddlewaretoken" model.csrf_token + ] + , expect = Http.expectJson JJCommitResponse + decode_command_response + + } + ) + + JJCommitResponse response -> case response of + Ok (Ok res) -> { model | log = MakeResult res.stdout res.stderr } |> nocmd + Ok (Err errmsg) -> { model | log = MakeError errmsg } |> nocmd + Err err -> { model | log = HttpError err } |> nocmd + + NoOp -> (model, Cmd.none) + +reload_log = + Http.get + { url = "log" + , expect = Http.expectString SetLog + } + +fetch_jj_status = + Http.get + { url = "jj/status" + , expect = Http.expectJson + (\r -> case r of + Ok response -> ReceiveJJStatus response + Err _ -> NoOp + ) + (JD.field "status" JD.string) + } + +subscriptions model = Sub.none + +link url text = + H.a + [ HA.href url] + [ H.text text ] + + +form : Model -> List (H.Attribute Msg) -> List (Html Msg) -> Html Msg +form model attrs children = + H.form + attrs + ( children + ++ [ H.input + [ HA.type_ "hidden" + , HA.name "csrfmiddlewaretoken" + , HA.value model.csrf_token + ] + [] + ] + ) + +as_percentage : Float -> String +as_percentage amount = (String.fromInt <| round (100 * amount)) ++ "%" + +view : Model -> Browser.Document Msg +view model = + { + title = model.file_path ++ " - " ++ model.slug ++ " - Thinks", + body = + [ header model + + , H.main_ + [ HA.classList + [ ("think-editor", True) + ] + , HA.attribute "style" <| ("--editor-size: " ++ (String.fromFloat model.editor_size)++"fr"++";--preview-size: "++(String.fromFloat (1-model.editor_size))++"fr") + ] + [ main_nav model + , log_pane model + , if model.is_dir then H.text "" else editor_pane model + , preview_pane model + ] + + , commit_modal model + ] + } + +header model = + H.header + [] + [ link "/" "thinks" + , H.h1 + [] + [ H.text model.slug ] + , H.p + [ HA.id "think-controls" ] + [ H.a + [ HA.target "preview" + , HA.href model.preview_url + ] + [ H.text "Preview" ] + , link "rename" "Rename" + , link "delete" "Delete" + , link ("/new/"++model.slug) "Remix" + , H.label + [ HA.for "editor-size-input" + ] + [ H.text "File editor size" ] + , H.input + [ HA.type_ "range" + , HA.id "editor-size-input" + , HA.min "0" + , HA.max "1" + , HA.step "0.05" + , HA.value <| String.fromFloat model.editor_size + , HE.on "input" (JD.at ["target", "valueAsNumber"] JD.float |> JD.map SetEditorSize) + , HA.list "size-values" + ] + [] + , H.output + [ HA.for "editor-size-input" ] + [ H.text <| as_percentage model.editor_size ] + , H.datalist + [ HA.id "size-values" ] + [ H.option [ HA.value "0.5" ] [] ] + ] + ] + +main_nav model = + H.details + [ HA.attribute "open" "" + , HA.id "main-nav" + , HE.preventDefaultOn "drop" (JD.at ["dataTransfer", "files"] (JD.list File.decoder) |> JD.map (\f -> (DropFiles Upload f, True)) ) + , HE.preventDefaultOn "dragover" (JD.succeed (NoOp,True)) + ] + [ H.summary + [] + [ H.text "Files" ] + , H.nav + [] + [ H.ul + [ HA.id "file-tree" ] + (List.map (\f -> + H.li + [ HA.classList + [ ("dir", f.is_dir) + , ("file", not f.is_dir) + , ("file-path", True) + ] + ] + [ link (file_edit_url f) f.name ] + ) model.files + ) + , form model + [ HA.id "file-form" + , HA.method "GET" + , HA.action "edit" + ] + [ H.input + [ HA.attribute "aria-labelledby" "new-file-button" + , HA.id "new-file-path" + , HA.type_ "text" + , HA.name "path" + , HA.value <| (if model.is_dir then model.file_path else String.join "/" <| List.reverse <| List.drop 1 <| List.reverse <| String.split "/" model.file_path)++"/" + ] + [] + , H.button + [ HA.id "new-file-button" + , HA.type_ "submit" + ] + [ H.text "New file" ] + ] + , form model + [ HE.onSubmit <| RunCommand model.command_to_run + ] + [ H.input + [ HA.attribute "aria-labelledby" "run-command-button" + , HE.onInput SetCommand + , HA.name "command" + , HA.value model.command_to_run + ] + [] + , H.button + [ HA.id "run-command-button" + , HA.type_ "submit" + ] + [ H.text "Run" ] + ] + , if String.right 4 model.file_path == ".elm" then + form model + [ HE.onSubmit <| case model.selected_package of + "" -> NoOp + p -> RunCommand <| "bash -c \"echo 'Y' | elm install " ++ p ++ "\"" + ] + [ H.input + [ HA.list "elm-packages" + , HE.onInput SelectPackage + , HA.value <| model.selected_package + ] + [] + , H.node "datalist" + [ HA.id "elm-packages"] + (List.map (\p -> H.option [HA.value p.name] [H.text p.name]) model.elm_packages) + , H.button + [ HA.id "install-package-button" + , HA.type_ "submit" + ] + [ H.text "Install" ] + ] + else + H.text "" + , H.button + [ HA.id "start-commit-button" + , HA.type_ "button" + , HE.onClick ShowCommitModal + ] + [ H.text "Commit" ] + ] + ] + +editor_pane model = + H.section + [ HA.id "editor" + , HE.preventDefaultOn "drop" (JD.at ["dataTransfer", "files"] (JD.list File.decoder) |> JD.map (\f -> (DropFiles Content f, True)) ) + , HE.preventDefaultOn "dragover" (JD.succeed (NoOp,True)) + ] + [ H.nav + [ HA.id "editor-controls" ] + [ H.span + [ HA.id "file-path" + , HA.class "file-path" + ] + [ H.text model.file_path ] + , H.span + [ HA.id "file-changed-status" + , HA.classList + [ ("changed", model.content_changed) ] + ] + [ H.text <| if model.content_changed then "changed" else "saved" ] + , H.details + [] + [ H.summary + [] + [ H.text "Actions" ] + , form model + [ HA.method "POST" + , HA.action "delete-file" + ] + [ H.input + [ HA.type_ "hidden" + , HA.name "path" + , HA.value model.file_path + ] + [] + , H.button + [ HA.type_ "submit" ] + [ H.text "Delete" ] + ] + , form model + [ HA.method "POST" + , HA.action "rename-file" + ] + [ H.input + [ HA.type_ "hidden" + , HA.name "path" + , HA.value model.file_path + ] + [] + , H.input + [ HA.type_ "text" + , HA.name "newpath" + , HA.value model.file_path + ] + [] + , H.button + [ HA.type_ "submit" ] + [ H.text "Rename" ] + ] + ] + ] + , form model + [ HA.id "file-form" + , HA.method "POST" + , HA.action "save-file" + ] + [ H.node "code-editor" + [ HE.on "change" (JD.at ["target", "value"] JD.string |> JD.map SetFileContent) + , HA.attribute "content" model.file_content + ] + [ H.text model.file_content ] + , H.input + [ HA.name "path" + , HA.value model.file_path + , HA.type_ "hidden" + ] + [] + , H.input + [ HA.name "content" + , HA.value model.file_content + , HA.type_ "hidden" + ] + [] + ] + ] + +log_pane : Model -> Html Msg +log_pane model = + H.details + ([HA.id "log" + , HE.on "toggle" (JD.at ["target", "open"] JD.bool |> JD.map ToggleLog) + ]++(if model.show_log then [ HA.attribute "open" ""] else []) + ) + [ H.summary + [] + [ H.text "Log" ] + , case model.log of + NotLoaded -> H.text "Not loaded" + + MakeResult stdout stderr -> + H.dl + [] + [ H.dt [] [H.text "stdout" ] + , H.dd [] [H.pre [] [ H.text stdout ]] + , H.dt [] [H.text "stderr" ] + , H.dd [] [H.pre [] [ H.text stderr ]] + ] + + CommandResult stdout stderr -> + H.dl + [] + [ H.dt [] [H.text "stdout" ] + , H.dd [] [H.pre [] [ H.text stdout ]] + , H.dt [] [H.text "stderr" ] + , H.dd [] [H.pre [] [ H.text stderr ]] + ] + + MakeError err -> + H.div + [] + [ H.h2 [] [ H.text "Error"] + , H.pre [] [H.text err] + ] + + HttpError err -> + let + text = case err of + BadUrl url -> "Bad URL " ++ url + Timeout -> "Timeout" + NetworkError -> "Network error" + BadStatus code -> "Bad response code " ++ (String.fromInt code) + BadBody body -> "Bad body " ++ body + in + H.div + [] + [ H.h2 [] [ H.text "Network error"] + , H.pre [] [H.text text] + ] + ] + +preview_pane model = + H.details + [ HA.attribute "open" "" + , HA.id "preview" + , HE.on "toggle" (JD.at ["target", "open"] JD.bool |> JD.map TogglePreview) + ] + ( [ H.summary + [] + [ H.text "Preview" + ] + ] + ++ if model.show_preview then + [ H.iframe + [ HA.id "preview-frame" + , HA.src model.preview_url + ] + [] + ] + else + [] + ) + +commit_modal model = + let + view_status status = + H.tr + [] + [ H.td [] [ H.text <| status.path ] ] + in + H.node "dialog" + [ HA.id "commit-modal" ] + [ H.h2 [] [H.text "Commit"] + , H.pre [ HA.id "jj-status" ] [H.text model.jj_status] + , H.form + [ HE.on "submit" + ( JD.at ["submitter","value"] JD.string + |> JD.map (\v -> case v of + "cancel" -> NoOp + _ -> JJCommit + ) + ) + , HA.method "dialog" + ] + [ H.textarea + [ HA.value model.commit_message + , HE.onInput SetCommitMessage + ] + [] + , H.button + [ HA.value "cancel" + , HA.attribute "formmethod" "dialog" + ] + [ H.text "Cancel"] + , H.button + [ HA.type_ "submit" + ] + [ H.text "Commit"] + ] + ] \ No newline at end of file diff --git a/test.mjs b/test.mjs new file mode 100644 index 0000000..1d61271 --- /dev/null +++ b/test.mjs @@ -0,0 +1,10 @@ +import show_error from './show-error.mjs'; +import init_app from './dist/load-think-editor.mjs'; + +(async () => { + const compilation_error = await show_error; + if(compilation_error) { + return; + } + init_app(); +})() \ No newline at end of file