├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── icon.png ├── icon.psd ├── icon.sketch ├── screenshot.png └── social.psd ├── elm-constants.json ├── elm-designer.code-workspace ├── elm.json ├── makefile ├── package-lock.json ├── package.json └── src ├── Bootstrap └── Tab.elm ├── CodeGen.elm ├── Codecs.elm ├── Css.elm ├── Document.elm ├── DragDropHelper.elm ├── Fonts.elm ├── Html5 └── DragDrop.elm ├── Icons.elm ├── Imgbb.elm ├── Library.elm ├── Main.elm ├── Model.elm ├── Palette.elm ├── Ports.elm ├── Style ├── Background.elm ├── Border.elm ├── Font.elm ├── Input.elm ├── Layout.elm ├── Shadow.elm └── Theme.elm ├── Views ├── Common.elm ├── ContextMenuPopup.elm ├── Editor.elm ├── ElmUI.elm └── Inspector.elm ├── app.html ├── app.js ├── images ├── favicon.svg ├── icon.png ├── landing-background.jpg └── landing-screenshot.png ├── index.html └── scss ├── _element.scss ├── _inspector.scss ├── _page.scss ├── _pane.scss ├── _tree.scss ├── _ui.scss ├── _utilities.scss ├── _workspace.scss ├── app.scss └── bootstrap ├── _accordion.scss ├── _alert.scss ├── _badge.scss ├── _breadcrumb.scss ├── _button-group.scss ├── _buttons.scss ├── _card.scss ├── _carousel.scss ├── _close.scss ├── _containers.scss ├── _dropdown.scss ├── _forms.scss ├── _functions.scss ├── _grid.scss ├── _helpers.scss ├── _images.scss ├── _list-group.scss ├── _mixins.scss ├── _modal.scss ├── _nav.scss ├── _navbar.scss ├── _offcanvas.scss ├── _pagination.scss ├── _popover.scss ├── _progress.scss ├── _reboot.scss ├── _root.scss ├── _spinners.scss ├── _tables.scss ├── _toasts.scss ├── _tooltip.scss ├── _transitions.scss ├── _type.scss ├── _utilities.scss ├── _variables.scss ├── bootstrap-grid.scss ├── bootstrap-reboot.scss ├── bootstrap-utilities.scss ├── bootstrap.scss ├── forms ├── _floating-labels.scss ├── _form-check.scss ├── _form-control.scss ├── _form-range.scss ├── _form-select.scss ├── _form-text.scss ├── _input-group.scss ├── _labels.scss └── _validation.scss ├── helpers ├── _clearfix.scss ├── _colored-links.scss ├── _position.scss ├── _ratio.scss ├── _stretched-link.scss ├── _text-truncation.scss └── _visually-hidden.scss ├── mixins ├── _alert.scss ├── _border-radius.scss ├── _box-shadow.scss ├── _breakpoints.scss ├── _buttons.scss ├── _caret.scss ├── _clearfix.scss ├── _color-scheme.scss ├── _container.scss ├── _deprecate.scss ├── _forms.scss ├── _gradients.scss ├── _grid.scss ├── _image.scss ├── _list-group.scss ├── _lists.scss ├── _pagination.scss ├── _reset-text.scss ├── _resize.scss ├── _table-variants.scss ├── _text-truncate.scss ├── _transition.scss ├── _utilities.scss └── _visually-hidden.scss ├── utilities └── _api.scss └── vendor └── _rfs.scss /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # elm-package generated files 3 | elm-stuff 4 | # elm-repl generated files 5 | repl-temp-* 6 | # Node, Parcel 7 | node_modules 8 | dist 9 | .parcel-cache 10 | # Python 11 | __pycache__/ 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 0.4.2 • 2022-06-14 2 | 3 | ## New features and tweaks 4 | 5 | * Import and export current document as JSON file 6 | * Display image width, height and format in 'Info' section 7 | 8 | ## Bugs fixed 9 | 10 | * Fix labelHidden emit 11 | * Made image resizing more predictable when tweaking its width and height settings 12 | 13 | --- 14 | 15 | # Version 0.4.1 • 2022-05-04 16 | 17 | ## New features and tweaks 18 | 19 | * Implement collapsible inspector subpanels 20 | * Allow to tweak pages vertical spacing 21 | * Add images 'Info' section on inspector 22 | 23 | ## Bugs fixed 24 | 25 | * Remove up/down arrows in Firefox 26 | * Fix logic to select custom preset sizes 27 | 28 | --- 29 | 30 | # Version 0.4.0 • 2022-02-28 31 | 32 | Elm Designer is now a web app. No more Electron binaries to download. Yay! 33 | 34 | ## New features and tweaks 35 | 36 | * New multi-page, scrollable workspace 37 | * Reworked outline view and context menu 38 | * Add label color for text fields 39 | * Add inner/outer shadow type toggler (thanks @axelbdt!) 40 | 41 | ## Bugs fixed 42 | 43 | * Image doesn't collapse anymore when inserted into a row 44 | * "Insert" menu now allows only valid parent-child combinations 45 | 46 | --- 47 | 48 | # Version 0.3.0 • 2021-07-11 49 | 50 | ⚠️ **Document format is changed from previous version so you won't be able to load your existing designs.** 51 | 52 | ## New features and tweaks 53 | 54 | * Added undo/redo menu commands for destructive operations (thanks @CharlonTank!) 55 | * Added support for relative positioned children (above, in front, etc.) 56 | * Added support for element shadow 57 | * Added UI to specify form fields label position: above, below, left, right and hidden 58 | * Added UI for border style: solid, dashed and dotted 59 | 60 | ## Bugs fixed 61 | 62 | * Node offset and rotation values are not generated 63 | * Can't remove Label on text fields 64 | 65 | --- 66 | 67 | # Version 0.2.1 • 2021-02-20 68 | 69 | ## New features and tweaks 70 | 71 | * Show elements textual content in tree item labels 72 | * Added Inter font 73 | 74 | ## Bugs fixed 75 | 76 | * Honor height setting on form library elements 77 | * While on a inspector field hitting backspace do not delete the selected element anymore 78 | 79 | **Note**: document format is the same of the previous version so your designs will continue to work after the upgrade. 80 | 81 | --- 82 | 83 | # Version 0.2.0 • 2021-02-06 84 | 85 | ⚠️ **Document format is changed from previous version so you won't be able to load your existing designs.** 86 | 87 | ## New features and tweaks 88 | 89 | * Insert images into page via drag and drop 90 | * Added alignment button to toggle centering on/off 91 | * Added inspector settings to specify exact width and height pixel values 92 | * Added letter and word spacing styles to inspector 93 | * Drop library elements directly on the page or on outline pane 94 | * Use Electron API to show error messages 95 | * Added Source Sans Pro, Source Serif Pro, Source Mono Pro, Roboto Mono and Space Mono fonts 96 | * Show inherited font family and weight styles in gray and italic instead of showing them between parens 97 | * Mark the 'fold' in fixed height viewports 98 | 99 | ## Bugs fixed 100 | 101 | * Better handling of font weight adjust while changing family 102 | * Made the inline text editor to honor node text alignment 103 | * Allow selection of left/right aligned (floated) elements within their container 104 | 105 | --- 106 | 107 | # Version 0.1.1 • 2021-01-09 108 | 109 | ## New features and tweaks 110 | 111 | * Added an "Insert" menu. This makes more obvious how to add a new element to the page 112 | * Use up and down arrows to increment/decrement all the numeric fields in the inspector pane 113 | * Added element move right/left and move up/down fields to inspector pane. These match Elm UI's `Element.move*` functions 114 | * Grouped fonts in the font family dropdown 115 | * Added IBM Plex Sans, Alegreya Sans, Lora and Libre Baskerville Google Fonts 116 | 117 | ## Bugs fixed 118 | 119 | * Workspace area now scrolls horizontally when designed page is bigger than app window 120 | * Wired up background image sizing controls 121 | * Codegen: emit border corners values (thank you @CharlonTank) 122 | * Codegen: native font-stack is now specified correctly 123 | 124 | **Note**: document format is the same of the previous version so your designs will continue to work after the upgrade. 125 | 126 | --- 127 | 128 | # Version 0.1.0 • 2020-12-27 129 | 130 | First public version. 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020-present, Andrea Peltrin 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elm Designer—A code generator for Elm UI 2 | 3 | ![Elm Designer UI](./assets/screenshot.png) 4 | 5 | ## Current status 6 | 7 | The application is in early stages of development and [supports a subset][support] of [Elm UI][elmui]. 8 | 9 | Previously Elm Designer was an Electron app, you can still download older versions on the [Releases page][r]. 10 | 11 | ## New to Elm Designer? 12 | 13 | The [quick guide][guide] is waiting for you. 14 | 15 | ## About images 16 | 17 | Images added to the page are automatically uploaded to [Imgbb][imgbb] servers and will remain available for 180 days. 18 | 19 | ## Limitations 20 | 21 | - UI is still pretty crude since the app is in an exploratory phase and I'm trying out different ideas. 22 | - Color picker is quite limited at the moment since Elm Designer is using HTML 5 `input type=color`. Specifically you can't reset a color or specify `inherit`. See [#1][issue1] 23 | 24 | ## Build Elm Designer from sources 25 | 26 | Elm Designer uses [Parcel][2] to compile Elm and SASS source. To install all the needed dependencies type: 27 | 28 | npm ci 29 | 30 | To run it locally in dev mode type: 31 | 32 | make dev 33 | 34 | or if your prefer to turn off the Elm debugger type: 35 | 36 | make dev-no-debug 37 | 38 | **Note**: you will need a valid [Imgbb API][api] key to be able to upload images. API access is free and requires only to sign up to the service. Once you get the API key set the `IMGBB_API_KEY` environment variable or create a `.env` file in the repo root containing such key: 39 | 40 | IMGBB_API_KEY=your-API-key 41 | 42 | Then the build process will create a `Env.imgbbApiKey` value. 43 | 44 | ## Credits 45 | 46 | Elm Designer contains patched versions of the following packages: 47 | 48 | * [Elm Bootstrap][eb] is copyright (c) 2017, Magnus Rundberget 49 | * [Html5 Drag-Drop][hdd] is copyright (c) 2018, Martin Norbäck Olivers 50 | 51 | [2]: https://parceljs.org 52 | [d]: https://github.com/passiomatic/elm-designer/releases/tag/v0.3.0 53 | [issue1]: https://github.com/passiomatic/elm-designer/issues/1 54 | [elmui]: https://github.com/mdgriffith/elm-ui 55 | [r]: https://github.com/passiomatic/elm-designer/releases 56 | [api]: https://api.imgbb.com 57 | [imgbb]: https://imgbb.com 58 | [guide]: https://github.com/passiomatic/elm-designer/wiki/Quick-guide 59 | [eb]: https://github.com/rundis/elm-bootstrap 60 | [hdd]: https://github.com/norpan/elm-html5-drag-drop 61 | [support]: https://github.com/passiomatic/elm-designer/wiki/Elm-UI-support-status 62 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passiomatic/elm-designer/1df8420b5010514658d116117b9456e42611462d/assets/icon.png -------------------------------------------------------------------------------- /assets/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passiomatic/elm-designer/1df8420b5010514658d116117b9456e42611462d/assets/icon.psd -------------------------------------------------------------------------------- /assets/icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passiomatic/elm-designer/1df8420b5010514658d116117b9456e42611462d/assets/icon.sketch -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passiomatic/elm-designer/1df8420b5010514658d116117b9456e42611462d/assets/screenshot.png -------------------------------------------------------------------------------- /assets/social.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/passiomatic/elm-designer/1df8420b5010514658d116117b9456e42611462d/assets/social.psd -------------------------------------------------------------------------------- /elm-constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "src", 3 | "moduleName": "Env", 4 | "values": [ 5 | "IMGBB_API_KEY" 6 | ] 7 | } -------------------------------------------------------------------------------- /elm-designer.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "search.exclude": { 9 | "**/dist": true, 10 | "**/elm-stuff": true 11 | }, 12 | "makefile.extensionOutputFolder": "./.vscode" 13 | } 14 | } -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "TSFoster/elm-uuid": "4.1.0", 10 | "elm/browser": "1.0.2", 11 | "elm/core": "1.0.5", 12 | "elm/file": "1.0.5", 13 | "elm/html": "1.0.0", 14 | "elm/http": "2.0.0", 15 | "elm/json": "1.1.3", 16 | "elm/random": "1.0.0", 17 | "elm/svg": "1.0.1", 18 | "elm/time": "1.0.0", 19 | "elm-community/list-extra": "8.2.2", 20 | "elm-community/undo-redo": "3.0.0", 21 | "feathericons/elm-feather": "1.5.0", 22 | "jinjor/elm-contextmenu": "2.0.0", 23 | "joshforisha/elm-html-entities": "1.0.0", 24 | "justinmimbs/time-extra": "1.1.0", 25 | "mdgriffith/elm-ui": "1.1.8", 26 | "miniBill/elm-codec": "2.0.0", 27 | "mpizenberg/elm-pointer-events": "4.0.2", 28 | "rtfeldman/elm-hex": "1.0.0", 29 | "the-sett/elm-pretty-printer": "2.2.3", 30 | "the-sett/elm-syntax-dsl": "4.0.1", 31 | "zwilias/elm-rosetree": "1.4.0" 32 | }, 33 | "indirect": { 34 | "Chadtech/elm-bool-extra": "2.4.0", 35 | "TSFoster/elm-bytes-extra": "1.3.0", 36 | "TSFoster/elm-md5": "2.0.0", 37 | "TSFoster/elm-sha1": "2.1.1", 38 | "danfishgold/base64-bytes": "1.0.3", 39 | "elm/bytes": "1.0.8", 40 | "elm/parser": "1.1.0", 41 | "elm/url": "1.0.0", 42 | "elm/virtual-dom": "1.0.3", 43 | "elm-community/basics-extra": "4.0.0", 44 | "elm-community/json-extra": "4.2.0", 45 | "elm-community/maybe-extra": "5.0.0", 46 | "justinmimbs/date": "3.2.1", 47 | "rtfeldman/elm-iso8601-date-strings": "1.1.3", 48 | "stil4m/elm-syntax": "7.1.1", 49 | "stil4m/structured-writer": "1.0.2", 50 | "zwilias/elm-utf-tools": "2.0.1" 51 | } 52 | }, 53 | "test-dependencies": { 54 | "direct": {}, 55 | "indirect": { 56 | "avh4/elm-color": "1.0.0" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | all: dev 2 | 3 | constants: 4 | npm run constants 5 | 6 | build: clean constants 7 | npm run build 8 | 9 | serve: build 10 | npx serve dist 11 | 12 | build-no-maps: clean constants 13 | npm run build-no-maps 14 | 15 | build-optimize-2: build 16 | elm-optimize-level-2 src/Main.elm --output dist/app-optimized.js 17 | 18 | dev-no-debug: constants 19 | npm run dev-no-debug 20 | 21 | dev: constants 22 | npm run dev 23 | 24 | landing: constants 25 | npm run landing 26 | 27 | clean: 28 | npm run clean -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-designer", 3 | "productName": "Elm Designer", 4 | "version": "0.4.2", 5 | "description": "A code generator for Elm UI", 6 | "repository": "https://github.com/passiomatic/elm-designer", 7 | "devDependencies": { 8 | "@parcel/transformer-elm": "^2.8.3", 9 | "@parcel/transformer-sass": "^2.8.3", 10 | "elm": "^0.19.1-5", 11 | "elm-constants": "^1.0.0", 12 | "elm-format": "^0.8.7", 13 | "parcel": "^2.8.3" 14 | }, 15 | "dependencies": { 16 | "balloon-css": "^1.2.0" 17 | }, 18 | "browserslist": "> 0.5%, last 2 versions, not dead", 19 | "scripts": { 20 | "dev": "parcel src/app.html", 21 | "dev-no-debug": "NODE_ENV=production parcel src/app.html", 22 | "landing": "parcel src/index.html", 23 | "constants": "elm-constants", 24 | "build": "parcel build src/index.html", 25 | "build-no-maps": "parcel build src/index.html --no-source-maps", 26 | "clean": "rm -fR dist" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Css.elm: -------------------------------------------------------------------------------- 1 | module Css exposing 2 | ( colorToString 3 | , colorToStringWithHash 4 | , em 5 | , percent 6 | , px 7 | , scaleBy 8 | , stringToColor 9 | , translateBy 10 | ) 11 | 12 | {-| CSS properties-to-string helpers. 13 | -} 14 | 15 | import Bitwise 16 | import Element exposing (Color) 17 | import Hex 18 | import Html exposing (Attribute) 19 | 20 | 21 | 22 | -- TRANSFORM 23 | 24 | 25 | translateBy x y = 26 | -- TODO use translate3d 27 | "translate(" ++ px x ++ ", " ++ px y ++ ")" 28 | 29 | 30 | scaleBy value = 31 | "scale(" ++ String.fromFloat value ++ ")" 32 | 33 | 34 | 35 | -- UNITS 36 | 37 | 38 | px value = 39 | String.fromInt value ++ "px" 40 | 41 | 42 | em value = 43 | String.fromFloat value ++ "em" 44 | 45 | 46 | percent value = 47 | String.fromFloat value ++ "%" 48 | 49 | 50 | 51 | -- COLOR 52 | 53 | 54 | colorToString : Color -> String 55 | colorToString value = 56 | colorToString_ False value 57 | 58 | 59 | colorToStringWithHash = 60 | colorToString_ True 61 | 62 | 63 | colorToString_ : Bool -> Color -> String 64 | colorToString_ addHash value = 65 | let 66 | floatTo256 f = 67 | if f >= 1 then 68 | 255 69 | 70 | else 71 | floor (f * 256) 72 | 73 | converter = 74 | floatTo256 >> Hex.toString >> String.toUpper >> String.padLeft 2 '0' 75 | 76 | rgba = 77 | Element.toRgb value 78 | in 79 | (if addHash then 80 | "#" 81 | 82 | else 83 | "" 84 | ) 85 | ++ converter rgba.red 86 | ++ converter rgba.green 87 | ++ converter rgba.blue 88 | 89 | 90 | stringToColor : String -> Color 91 | stringToColor value = 92 | let 93 | rgb = 94 | -- Drop # 95 | (if String.startsWith "#" value then 96 | String.dropLeft 1 value 97 | 98 | else 99 | value 100 | ) 101 | |> String.toLower 102 | |> Hex.fromString 103 | |> Result.withDefault 0 104 | 105 | r = 106 | Bitwise.shiftRightBy 16 rgb 107 | |> Bitwise.and 0xFF 108 | 109 | g = 110 | Bitwise.shiftRightBy 8 rgb 111 | |> Bitwise.and 0xFF 112 | 113 | b = 114 | Bitwise.and 0xFF rgb 115 | in 116 | Element.fromRgb255 117 | { red = r 118 | , green = g 119 | , blue = b 120 | , alpha = 1.0 121 | } 122 | -------------------------------------------------------------------------------- /src/DragDropHelper.elm: -------------------------------------------------------------------------------- 1 | module DragDropHelper exposing (addDroppedNode, getDroppedNode, setDragImage) 2 | 3 | {-| Drag and drop helpers. 4 | -} 5 | 6 | import Document exposing (DragId(..), DropId(..), Node) 7 | import Json.Decode as Decode exposing (Decoder, Value) 8 | import Model exposing (..) 9 | import Ports 10 | import Style.Layout as Layout exposing (..) 11 | import Tree exposing (Tree) 12 | import Tree.Zipper as Zipper exposing (Zipper) 13 | import UUID exposing (Seeds, UUID) 14 | 15 | 16 | {-| Figure out _what_ user just dropped. 17 | -} 18 | getDroppedNode : Model -> DragId -> { x : Float, y : Float } -> ( Seeds, Maybe (Tree Node), Zipper Node ) 19 | getDroppedNode model dragId position = 20 | case dragId of 21 | Move node -> 22 | case Document.selectNodeWith node.id model.document.present of 23 | Just zipper -> 24 | if model.isAltDown then 25 | -- Duplicate node 26 | let 27 | ( newSeeds, newNode ) = 28 | Document.duplicateNode zipper model.seeds 29 | in 30 | ( newSeeds, Just newNode, zipper ) 31 | 32 | else 33 | -- Move node 34 | let 35 | newZipper = 36 | Document.removeNode zipper 37 | in 38 | ( model.seeds, Just (Zipper.tree zipper), newZipper ) 39 | 40 | Nothing -> 41 | ( model.seeds, Nothing, model.document.present ) 42 | 43 | Drag node -> 44 | case Document.selectNodeWith node.id model.document.present of 45 | Just zipper -> 46 | let 47 | -- Update node at new position 48 | newNode = 49 | Zipper.mapLabel 50 | (\node_ -> 51 | { node_ 52 | | offsetX = position.x 53 | , offsetY = position.y 54 | } 55 | ) 56 | zipper 57 | |> Zipper.tree 58 | 59 | newZipper = 60 | Document.removeNode zipper 61 | in 62 | ( model.seeds, Just newNode, newZipper ) 63 | 64 | Nothing -> 65 | ( model.seeds, Nothing, model.document.present ) 66 | 67 | Insert node -> 68 | let 69 | indexer type_ = 70 | Document.getNextIndexFor type_ (Zipper.root model.document.present) 71 | 72 | ( newSeeds, newNode ) = 73 | Document.fromTemplateAt position node model.seeds indexer 74 | in 75 | ( newSeeds, Just newNode, model.document.present ) 76 | 77 | 78 | {-| Figure out _where_ user just dropped the node. 79 | -} 80 | addDroppedNode : Model -> DropId -> Tree Node -> Zipper Node -> Zipper Node 81 | addDroppedNode model dropId node zipper = 82 | case dropId of 83 | -- Insert new element just before the sibling 84 | InsertBefore siblingId -> 85 | Document.insertNodeBefore siblingId node zipper 86 | 87 | -- Insert new element just after the sibling 88 | InsertAfter siblingId -> 89 | Document.insertNodeAfter siblingId node zipper 90 | 91 | -- Add new element as last child 92 | AppendTo parentId -> 93 | case Document.selectNodeWith parentId zipper of 94 | Just zipper_ -> 95 | Document.appendNode node zipper_ 96 | 97 | Nothing -> 98 | zipper 99 | 100 | 101 | setDragImage dragStart = 102 | case dragStart.dragId of 103 | Drag _ -> 104 | Ports.setDragImage { event = dragStart.event, dragging = True } 105 | 106 | _ -> 107 | Ports.setDragImage { event = dragStart.event, dragging = False } 108 | -------------------------------------------------------------------------------- /src/Imgbb.elm: -------------------------------------------------------------------------------- 1 | module Imgbb exposing (track, uploadNextFile) 2 | 3 | {-| Upload an image using Imgbb service and track progress. 4 | 5 | Typical response after a successul upload is: 6 | 7 | { 8 | "data": { 9 | "url": "https://i.ibb.co/6NMDgSB/sample.jpg", 10 | "width": 2333, 11 | "height": 3500, 12 | ... 13 | "image": { 14 | "mime": "image/jpeg", 15 | ... 16 | } 17 | }, 18 | "success": true, 19 | "status": 200 20 | } 21 | 22 | See for the full API documentation. 23 | 24 | -} 25 | 26 | import Document exposing (ImageData) 27 | import File exposing (File) 28 | import Http exposing (Error) 29 | import Json.Decode as D exposing (Decoder) 30 | import Model exposing (Msg(..), UploadState(..)) 31 | 32 | 33 | endpointUrl = 34 | -- Set expiration to 180 days 35 | "https://api.imgbb.com/1/upload?expiration=15552000&key=" 36 | 37 | 38 | uploadTimeout = 39 | 60 * 1000 40 | 41 | 42 | uploadNextFile key files = 43 | case files of 44 | next :: others -> 45 | ( Uploading next others 0 46 | , postTo key next 47 | ) 48 | 49 | [] -> 50 | ( Ready 51 | , Cmd.none 52 | ) 53 | 54 | 55 | postTo : String -> File -> Cmd Msg 56 | postTo key file = 57 | Http.request 58 | { method = "POST" 59 | , headers = [] 60 | , url = endpointUrl ++ key 61 | , body = 62 | Http.multipartBody 63 | [ Http.filePart "image" file 64 | ] 65 | , expect = Http.expectJson FileUploaded responseDecoder 66 | , timeout = Just uploadTimeout 67 | , tracker = Just (File.name file) 68 | } 69 | 70 | 71 | track current others = 72 | Http.track (File.name current) (FileUploading current others) 73 | 74 | 75 | responseDecoder : Decoder ImageData 76 | responseDecoder = 77 | D.map5 ImageData 78 | (D.at [ "data", "url" ] D.string) 79 | (D.succeed "") 80 | (D.at [ "data", "width" ] (D.maybe D.int)) 81 | (D.at [ "data", "height" ] (D.maybe D.int)) 82 | (D.at [ "data", "image", "mime" ] (D.maybe D.string)) 83 | -------------------------------------------------------------------------------- /src/Palette.elm: -------------------------------------------------------------------------------- 1 | module Palette exposing 2 | ( black 3 | , blue 4 | , brown 5 | , charcoal 6 | , darkBlue 7 | , darkBrown 8 | , darkCharcoal 9 | , darkGray 10 | , darkGreen 11 | , darkGrey 12 | , darkOrange 13 | , darkPurple 14 | , darkRed 15 | , darkYellow 16 | , gray 17 | , green 18 | , grey 19 | , lightBlue 20 | , lightBrown 21 | , lightCharcoal 22 | , lightGray 23 | , lightGreen 24 | , lightGrey 25 | , lightOrange 26 | , lightPurple 27 | , lightRed 28 | , lightYellow 29 | , orange 30 | , purple 31 | , red 32 | , transparent 33 | , white 34 | , yellow 35 | ) 36 | 37 | {-| These colors come from the Tango palette which provides aesthetically reasonable defaults for colors. 38 | 39 | Each color also comes with a light and dark version. 40 | 41 | Adapted from: 42 | 43 | -} 44 | 45 | import Element exposing (Color) 46 | 47 | 48 | lightRed : Color 49 | lightRed = 50 | Element.rgba (239 / 255) (41 / 255) (41 / 255) 1.0 51 | 52 | 53 | red : Color 54 | red = 55 | Element.rgba (204 / 255) (0 / 255) (0 / 255) 1.0 56 | 57 | 58 | darkRed : Color 59 | darkRed = 60 | Element.rgba (164 / 255) (0 / 255) (0 / 255) 1.0 61 | 62 | 63 | lightOrange : Color 64 | lightOrange = 65 | Element.rgba (252 / 255) (175 / 255) (62 / 255) 1.0 66 | 67 | 68 | orange : Color 69 | orange = 70 | Element.rgba (245 / 255) (121 / 255) (0 / 255) 1.0 71 | 72 | 73 | darkOrange : Color 74 | darkOrange = 75 | Element.rgba (206 / 255) (92 / 255) (0 / 255) 1.0 76 | 77 | 78 | lightYellow : Color 79 | lightYellow = 80 | Element.rgba (255 / 255) (233 / 255) (79 / 255) 1.0 81 | 82 | 83 | yellow : Color 84 | yellow = 85 | Element.rgba (237 / 255) (212 / 255) (0 / 255) 1.0 86 | 87 | 88 | darkYellow : Color 89 | darkYellow = 90 | Element.rgba (196 / 255) (160 / 255) (0 / 255) 1.0 91 | 92 | 93 | lightGreen : Color 94 | lightGreen = 95 | Element.rgba (138 / 255) (226 / 255) (52 / 255) 1.0 96 | 97 | 98 | green : Color 99 | green = 100 | Element.rgba (115 / 255) (210 / 255) (22 / 255) 1.0 101 | 102 | 103 | darkGreen : Color 104 | darkGreen = 105 | Element.rgba (78 / 255) (154 / 255) (6 / 255) 1.0 106 | 107 | 108 | lightBlue : Color 109 | lightBlue = 110 | Element.rgba (114 / 255) (159 / 255) (207 / 255) 1.0 111 | 112 | 113 | blue : Color 114 | blue = 115 | Element.rgba (52 / 255) (101 / 255) (164 / 255) 1.0 116 | 117 | 118 | darkBlue : Color 119 | darkBlue = 120 | Element.rgba (32 / 255) (74 / 255) (135 / 255) 1.0 121 | 122 | 123 | lightPurple : Color 124 | lightPurple = 125 | Element.rgba (173 / 255) (127 / 255) (168 / 255) 1.0 126 | 127 | 128 | purple : Color 129 | purple = 130 | Element.rgba (117 / 255) (80 / 255) (123 / 255) 1.0 131 | 132 | 133 | darkPurple : Color 134 | darkPurple = 135 | Element.rgba (92 / 255) (53 / 255) (102 / 255) 1.0 136 | 137 | 138 | lightBrown : Color 139 | lightBrown = 140 | Element.rgba (233 / 255) (185 / 255) (110 / 255) 1.0 141 | 142 | 143 | brown : Color 144 | brown = 145 | Element.rgba (193 / 255) (125 / 255) (17 / 255) 1.0 146 | 147 | 148 | darkBrown : Color 149 | darkBrown = 150 | Element.rgba (143 / 255) (89 / 255) (2 / 255) 1.0 151 | 152 | 153 | black : Color 154 | black = 155 | Element.rgba (0 / 255) (0 / 255) (0 / 255) 1.0 156 | 157 | 158 | white : Color 159 | white = 160 | Element.rgba (255 / 255) (255 / 255) (255 / 255) 1.0 161 | 162 | 163 | lightGrey : Color 164 | lightGrey = 165 | Element.rgba (238 / 255) (238 / 255) (236 / 255) 1.0 166 | 167 | 168 | grey : Color 169 | grey = 170 | Element.rgba (211 / 255) (215 / 255) (207 / 255) 1.0 171 | 172 | 173 | darkGrey : Color 174 | darkGrey = 175 | Element.rgba (186 / 255) (189 / 255) (182 / 255) 1.0 176 | 177 | 178 | lightGray : Color 179 | lightGray = 180 | Element.rgba (238 / 255) (238 / 255) (236 / 255) 1.0 181 | 182 | 183 | gray : Color 184 | gray = 185 | Element.rgba (211 / 255) (215 / 255) (207 / 255) 1.0 186 | 187 | 188 | darkGray : Color 189 | darkGray = 190 | Element.rgba (186 / 255) (189 / 255) (182 / 255) 1.0 191 | 192 | 193 | lightCharcoal : Color 194 | lightCharcoal = 195 | Element.rgba (136 / 255) (138 / 255) (133 / 255) 1.0 196 | 197 | 198 | charcoal : Color 199 | charcoal = 200 | Element.rgba (85 / 255) (87 / 255) (83 / 255) 1.0 201 | 202 | 203 | darkCharcoal : Color 204 | darkCharcoal = 205 | Element.rgba (46 / 255) (52 / 255) (54 / 255) 1.0 206 | 207 | 208 | transparent : Color 209 | transparent = 210 | Element.rgba 1.0 1.0 1.0 0.0 211 | -------------------------------------------------------------------------------- /src/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Ports exposing 2 | ( copyToClipboard 3 | , endDrag 4 | , loadDocument 5 | , onDocumentLoad 6 | , saveDocument 7 | , selectText 8 | , setDragImage 9 | , setFontLinks 10 | , showNotification 11 | , toggleDialog 12 | ) 13 | 14 | import Json.Decode exposing (Value) 15 | import Model exposing (..) 16 | 17 | 18 | 19 | -- PORTS OUT 20 | 21 | 22 | port saveDocument : String -> Cmd msg 23 | 24 | 25 | port loadDocument : () -> Cmd msg 26 | 27 | 28 | port copyToClipboard : String -> Cmd msg 29 | 30 | 31 | port selectText : String -> Cmd msg 32 | 33 | 34 | port setFontLinks : List String -> Cmd msg 35 | 36 | 37 | port setDragImage : { event : Value, dragging : Bool } -> Cmd msg 38 | 39 | 40 | port endDrag : () -> Cmd msg 41 | 42 | 43 | port toggleDialog : () -> Cmd msg 44 | 45 | 46 | port showNotification : 47 | { title : String 48 | , message : String 49 | } 50 | -> Cmd msg 51 | 52 | 53 | 54 | -- PORTS IN 55 | 56 | 57 | port onDocumentLoad : (String -> msg) -> Sub msg 58 | -------------------------------------------------------------------------------- /src/Style/Background.elm: -------------------------------------------------------------------------------- 1 | module Style.Background exposing 2 | ( Background(..) 3 | , isImage 4 | , isNone 5 | , isSolid 6 | , setBackground 7 | , solid 8 | ) 9 | 10 | {-| Background appearance properties. 11 | -} 12 | 13 | import Element exposing (Color) 14 | 15 | 16 | type Background 17 | = Solid Color 18 | | Image String 19 | | None 20 | 21 | 22 | solid : Color -> Background 23 | solid color = 24 | Solid color 25 | 26 | 27 | setBackground : Background -> { a | background : Background } -> { a | background : Background } 28 | setBackground value node = 29 | { node | background = value } 30 | 31 | 32 | isSolid value = 33 | case value of 34 | Solid _ -> 35 | True 36 | 37 | _ -> 38 | False 39 | 40 | 41 | isImage value = 42 | case value of 43 | Image _ -> 44 | True 45 | 46 | _ -> 47 | False 48 | 49 | 50 | isNone value = 51 | case value of 52 | None -> 53 | True 54 | 55 | _ -> 56 | False 57 | -------------------------------------------------------------------------------- /src/Style/Border.elm: -------------------------------------------------------------------------------- 1 | module Style.Border exposing 2 | ( BorderCorner 3 | , BorderStyle(..) 4 | , BorderWidth 5 | , corner 6 | , hasWidth 7 | , isDashed 8 | , isDotted 9 | , isRounded 10 | , isSolid 11 | , setBottomLeftCorner 12 | , setBottomRightCorner 13 | , setBottomWidth 14 | , setColor 15 | , setCorner 16 | , setLeftWidth 17 | , setRightWidth 18 | , setStyle 19 | , setTopLeftCorner 20 | , setTopRightCorner 21 | , setTopWidth 22 | , setWidth 23 | , width 24 | ) 25 | 26 | {-| Border properties. 27 | -} 28 | 29 | import Element exposing (Color) 30 | 31 | 32 | type alias BorderWidth = 33 | { locked : Bool 34 | , top : Int 35 | , right : Int 36 | , bottom : Int 37 | , left : Int 38 | } 39 | 40 | 41 | width : Int -> BorderWidth 42 | width value = 43 | BorderWidth True value value value value 44 | 45 | 46 | hasWidth : BorderWidth -> Bool 47 | hasWidth value = 48 | value.top /= 0 || value.right /= 0 || value.bottom /= 0 || value.left /= 0 49 | 50 | 51 | setWidth : BorderWidth -> { a | borderWidth : BorderWidth } -> { a | borderWidth : BorderWidth } 52 | setWidth value node = 53 | { node | borderWidth = value } 54 | 55 | 56 | setColor : Color -> { a | borderColor : Color } -> { a | borderColor : Color } 57 | setColor value node = 58 | { node | borderColor = value } 59 | 60 | 61 | setTopWidth : Int -> BorderWidth -> BorderWidth 62 | setTopWidth value record = 63 | if record.locked then 64 | width value 65 | 66 | else 67 | { record | top = value } 68 | 69 | 70 | setRightWidth : Int -> BorderWidth -> BorderWidth 71 | setRightWidth value record = 72 | if record.locked then 73 | width value 74 | 75 | else 76 | { record | right = value } 77 | 78 | 79 | setBottomWidth : Int -> BorderWidth -> BorderWidth 80 | setBottomWidth value record = 81 | if record.locked then 82 | width value 83 | 84 | else 85 | { record | bottom = value } 86 | 87 | 88 | setLeftWidth : Int -> BorderWidth -> BorderWidth 89 | setLeftWidth value record = 90 | if record.locked then 91 | width value 92 | 93 | else 94 | { record | left = value } 95 | 96 | 97 | type BorderStyle 98 | = Solid 99 | | Dashed 100 | | Dotted 101 | 102 | 103 | type alias BorderCorner = 104 | { locked : Bool 105 | , topLeft : Int 106 | , topRight : Int 107 | , bottomRight : Int 108 | , bottomLeft : Int 109 | } 110 | 111 | 112 | corner : Int -> BorderCorner 113 | corner value = 114 | BorderCorner True value value value value 115 | 116 | 117 | setCorner : BorderCorner -> { a | borderCorner : BorderCorner } -> { a | borderCorner : BorderCorner } 118 | setCorner value node = 119 | { node | borderCorner = value } 120 | 121 | 122 | setStyle : BorderStyle -> { a | borderStyle : BorderStyle } -> { a | borderStyle : BorderStyle } 123 | setStyle value node = 124 | { node | borderStyle = value } 125 | 126 | 127 | setTopLeftCorner : Int -> BorderCorner -> BorderCorner 128 | setTopLeftCorner value record = 129 | if record.locked then 130 | corner value 131 | 132 | else 133 | { record | topLeft = value } 134 | 135 | 136 | setTopRightCorner : Int -> BorderCorner -> BorderCorner 137 | setTopRightCorner value record = 138 | if record.locked then 139 | corner value 140 | 141 | else 142 | { record | topRight = value } 143 | 144 | 145 | setBottomRightCorner : Int -> BorderCorner -> BorderCorner 146 | setBottomRightCorner value record = 147 | if record.locked then 148 | corner value 149 | 150 | else 151 | { record | bottomRight = value } 152 | 153 | 154 | setBottomLeftCorner : Int -> BorderCorner -> BorderCorner 155 | setBottomLeftCorner value record = 156 | if record.locked then 157 | corner value 158 | 159 | else 160 | { record | bottomLeft = value } 161 | 162 | 163 | isRounded : BorderCorner -> Bool 164 | isRounded value = 165 | value.topLeft /= 0 || value.topRight /= 0 || value.bottomLeft /= 0 || value.bottomRight /= 0 166 | 167 | 168 | isSolid value = 169 | case value of 170 | Solid -> 171 | True 172 | 173 | _ -> 174 | False 175 | 176 | 177 | isDashed value = 178 | case value of 179 | Dashed -> 180 | True 181 | 182 | _ -> 183 | False 184 | 185 | 186 | isDotted value = 187 | case value of 188 | Dotted -> 189 | True 190 | 191 | _ -> 192 | False 193 | -------------------------------------------------------------------------------- /src/Style/Font.elm: -------------------------------------------------------------------------------- 1 | module Style.Font exposing 2 | ( FontFamily 3 | , FontType(..) 4 | , FontWeight(..) 5 | , Local(..) 6 | , TextAlignment(..) 7 | , findClosestWeight 8 | , minFontSizeAllowed 9 | , setColor 10 | , setFamily 11 | , setLetterSpacing 12 | , setSize 13 | , setTextAlignment 14 | , setWeight 15 | , setWordSpacing 16 | , weightName 17 | ) 18 | 19 | {-| Font properties. 20 | -} 21 | 22 | import Element exposing (Color) 23 | 24 | 25 | minFontSizeAllowed = 26 | 10 27 | 28 | 29 | {-| A style value, specified locally or inherited from parent element. 30 | 31 | In Elm UI only `Font.color`, `Font.size`, and `Font.family` are inherited. 32 | 33 | -} 34 | type Local a 35 | = Local a 36 | | Inherited 37 | 38 | 39 | type FontType 40 | = Native (List String) 41 | | External String 42 | 43 | 44 | type alias FontFamily = 45 | { name : String 46 | , type_ : FontType 47 | , weights : List FontWeight 48 | } 49 | 50 | 51 | setFamily : Local FontFamily -> { a | fontFamily : Local FontFamily } -> { a | fontFamily : Local FontFamily } 52 | setFamily value node = 53 | { node | fontFamily = value } 54 | 55 | 56 | setSize : Local Int -> { a | fontSize : Local Int } -> { a | fontSize : Local Int } 57 | setSize value node = 58 | { node | fontSize = value } 59 | 60 | 61 | setColor : Local Color -> { a | fontColor : Local Color } -> { a | fontColor : Local Color } 62 | setColor value node = 63 | { node | fontColor = value } 64 | 65 | 66 | setWeight : FontWeight -> { a | fontWeight : FontWeight } -> { a | fontWeight : FontWeight } 67 | setWeight value node = 68 | { node | fontWeight = value } 69 | 70 | 71 | setLetterSpacing : Float -> { b | letterSpacing : Float } -> { b | letterSpacing : Float } 72 | setLetterSpacing value node = 73 | { node | letterSpacing = value } 74 | 75 | 76 | setWordSpacing : Float -> { b | wordSpacing : Float } -> { b | wordSpacing : Float } 77 | setWordSpacing value node = 78 | { node | wordSpacing = value } 79 | 80 | 81 | {-| Unlike Elm UI a font weight tells more than how a font is lighter or darker. 82 | 83 | In fact, each font weight may have a paired italic version so here we expand the weight list to handle all cases. 84 | 85 | -} 86 | type FontWeight 87 | = Heavy 88 | | HeavyItalic 89 | | ExtraBold 90 | | ExtraBoldItalic 91 | | Bold 92 | | BoldItalic 93 | | SemiBold 94 | | SemiBoldItalic 95 | | Medium 96 | | MediumItalic 97 | | Regular 98 | | Italic 99 | | Light 100 | | LightItalic 101 | | ExtraLight 102 | | ExtraLightItalic 103 | | Hairline 104 | | HairlineItalic 105 | 106 | 107 | {-| Prettified font weight names. 108 | -} 109 | weightName : FontWeight -> String 110 | weightName value = 111 | case value of 112 | Heavy -> 113 | "Heavy" 114 | 115 | HeavyItalic -> 116 | "Heavy Italic" 117 | 118 | ExtraBold -> 119 | "Extra-bold" 120 | 121 | ExtraBoldItalic -> 122 | "Extra-bold Italic" 123 | 124 | Bold -> 125 | "Bold" 126 | 127 | BoldItalic -> 128 | "Bold Italic" 129 | 130 | SemiBold -> 131 | "Semi-bold" 132 | 133 | SemiBoldItalic -> 134 | "Semi-bold Italic" 135 | 136 | Medium -> 137 | "Medium" 138 | 139 | MediumItalic -> 140 | "Medium Italic" 141 | 142 | Regular -> 143 | "Regular" 144 | 145 | Italic -> 146 | "Italic" 147 | 148 | Light -> 149 | "Light" 150 | 151 | LightItalic -> 152 | "Light Italic" 153 | 154 | ExtraLight -> 155 | "Extra-light" 156 | 157 | ExtraLightItalic -> 158 | "Extra-light Italic" 159 | 160 | Hairline -> 161 | "Hairline" 162 | 163 | HairlineItalic -> 164 | "Hairline Italic" 165 | 166 | 167 | findClosestWeight : FontWeight -> List FontWeight -> FontWeight 168 | findClosestWeight optimal weights = 169 | weights 170 | |> List.map 171 | (\weight -> 172 | ( weight, abs (weightNumber weight - weightNumber optimal) ) 173 | ) 174 | |> List.sortBy Tuple.second 175 | |> List.head 176 | |> Maybe.map Tuple.first 177 | |> Maybe.withDefault Regular 178 | 179 | 180 | {-| Mapping between weight and CSS values. 181 | -} 182 | weightNumber : FontWeight -> Int 183 | weightNumber value = 184 | case value of 185 | Heavy -> 186 | 900 187 | 188 | HeavyItalic -> 189 | 901 190 | 191 | ExtraBold -> 192 | 800 193 | 194 | ExtraBoldItalic -> 195 | 801 196 | 197 | Bold -> 198 | 700 199 | 200 | BoldItalic -> 201 | 701 202 | 203 | SemiBold -> 204 | 600 205 | 206 | SemiBoldItalic -> 207 | 601 208 | 209 | Medium -> 210 | 500 211 | 212 | MediumItalic -> 213 | 501 214 | 215 | Regular -> 216 | 400 217 | 218 | Italic -> 219 | 401 220 | 221 | Light -> 222 | 300 223 | 224 | LightItalic -> 225 | 301 226 | 227 | ExtraLight -> 228 | 200 229 | 230 | ExtraLightItalic -> 231 | 201 232 | 233 | Hairline -> 234 | 100 235 | 236 | HairlineItalic -> 237 | 101 238 | 239 | 240 | type TextAlignment 241 | = TextCenter 242 | | TextStart 243 | | TextEnd 244 | | TextJustify 245 | 246 | 247 | setTextAlignment : TextAlignment -> { a | textAlignment : TextAlignment } -> { a | textAlignment : TextAlignment } 248 | setTextAlignment value node = 249 | { node | textAlignment = value } 250 | 251 | 252 | 253 | -- type FontTransform 254 | -- = Uppercase 255 | -- | Lowercase 256 | -- | Capitalize 257 | -- | SmallCaps 258 | -------------------------------------------------------------------------------- /src/Style/Input.elm: -------------------------------------------------------------------------------- 1 | module Style.Input exposing 2 | ( LabelPosition(..) 3 | , labelPositionName 4 | , setLabelColor 5 | , setLabelPosition 6 | ) 7 | 8 | import Element exposing (Color) 9 | import Style.Font exposing (Local) 10 | 11 | 12 | {-| Label properties. 13 | -} 14 | type LabelPosition 15 | = LabelAbove 16 | | LabelBelow 17 | | LabelLeft 18 | | LabelRight 19 | | LabelHidden 20 | 21 | 22 | labelPositionName position = 23 | case position of 24 | LabelAbove -> 25 | "Above" 26 | 27 | LabelBelow -> 28 | "Below" 29 | 30 | LabelLeft -> 31 | "Left" 32 | 33 | LabelRight -> 34 | "Right" 35 | 36 | LabelHidden -> 37 | "Hidden" 38 | 39 | 40 | setLabelPosition : LabelPosition -> { a | position : LabelPosition } -> { a | position : LabelPosition } 41 | setLabelPosition value record = 42 | { record | position = value } 43 | 44 | 45 | setLabelColor : Local Color -> { a | color : Local Color } -> { a | color : Local Color } 46 | setLabelColor value record = 47 | { record | color = value } 48 | -------------------------------------------------------------------------------- /src/Style/Layout.elm: -------------------------------------------------------------------------------- 1 | module Style.Layout exposing 2 | ( Alignment(..) 3 | , Length(..) 4 | , Padding 5 | , Position(..) 6 | , Spacing(..) 7 | , fill 8 | , fit 9 | , padding 10 | , paddingXY 11 | , portion 12 | , positionName 13 | , px 14 | , setLock 15 | , setOffsetX 16 | , setOffsetY 17 | , setPadding 18 | , setPaddingBottom 19 | , setPaddingLeft 20 | , setPaddingRight 21 | , setPaddingTop 22 | , setPosition 23 | , setSpacing 24 | , setSpacingX 25 | , setSpacingY 26 | , setWidthMax 27 | , setWidthMin 28 | , spacing 29 | , spacingXY 30 | , unspecified 31 | ) 32 | 33 | {-| These types mirrors the Elm UI ones as much as possible. 34 | 35 | The idea is to not reinvent another vocabulary to describe the UI 36 | elements but to stick to what we are going to serialize in the end. 37 | 38 | We basically reconstruct several Elm UI opaque types, thus allowing 39 | to "case-of" on those. 40 | 41 | -} 42 | 43 | 44 | {-| Element length, used as width or height. 45 | -} 46 | type Length 47 | = Px Int 48 | | Content 49 | | Fill Int 50 | | Unspecified 51 | 52 | 53 | px : Int -> Length 54 | px value = 55 | Px value 56 | 57 | 58 | fit : Length 59 | fit = 60 | Content 61 | 62 | 63 | fill : Length 64 | fill = 65 | Fill 1 66 | 67 | 68 | portion : Int -> Length 69 | portion value = 70 | Fill value 71 | 72 | 73 | unspecified = 74 | Unspecified 75 | 76 | 77 | setWidthMin : Maybe Int -> { a | widthMin : Maybe Int } -> { a | widthMin : Maybe Int } 78 | setWidthMin value node = 79 | { node | widthMin = value } 80 | 81 | 82 | setWidthMax : Maybe Int -> { a | widthMax : Maybe Int } -> { a | widthMax : Maybe Int } 83 | setWidthMax value node = 84 | { node | widthMax = value } 85 | 86 | 87 | 88 | -- POSITION 89 | 90 | 91 | type Position 92 | = Above 93 | | Below 94 | | OnStart 95 | | OnEnd 96 | | InFront 97 | | BehindContent 98 | | Normal 99 | 100 | 101 | positionName : Position -> Maybe String 102 | positionName position = 103 | case position of 104 | Above -> 105 | Just "Above" 106 | 107 | Below -> 108 | Just "Below" 109 | 110 | OnStart -> 111 | Just "Left" 112 | 113 | OnEnd -> 114 | Just "Right" 115 | 116 | InFront -> 117 | Just "In Front" 118 | 119 | BehindContent -> 120 | Just "Behind Content" 121 | 122 | Normal -> 123 | Nothing 124 | 125 | 126 | setPosition : Position -> { a | position : Position } -> { a | position : Position } 127 | setPosition value node = 128 | { node | position = value } 129 | 130 | 131 | 132 | -- ALIGNMENT 133 | 134 | 135 | type Alignment 136 | = Center 137 | | Start 138 | | End 139 | | None 140 | 141 | 142 | 143 | -- PADDING 144 | 145 | 146 | type alias Padding = 147 | { locked : Bool 148 | , top : Int 149 | , right : Int 150 | , bottom : Int 151 | , left : Int 152 | } 153 | 154 | 155 | {-| Setup a locked padding value. 156 | -} 157 | padding : Int -> Padding 158 | padding value = 159 | Padding True value value value value 160 | 161 | 162 | {-| Setup a unlocked padding value. 163 | -} 164 | paddingXY : Int -> Int -> Padding 165 | paddingXY x y = 166 | Padding False y x y x 167 | 168 | 169 | setPadding : Padding -> { a | padding : Padding } -> { a | padding : Padding } 170 | setPadding value node = 171 | { node | padding = value } 172 | 173 | 174 | setPaddingTop : Int -> Padding -> Padding 175 | setPaddingTop value padding_ = 176 | if padding_.locked then 177 | padding value 178 | 179 | else 180 | { padding_ | top = value } 181 | 182 | 183 | setPaddingRight : Int -> Padding -> Padding 184 | setPaddingRight value padding_ = 185 | if padding_.locked then 186 | padding value 187 | 188 | else 189 | { padding_ | right = value } 190 | 191 | 192 | setPaddingBottom : Int -> Padding -> Padding 193 | setPaddingBottom value padding_ = 194 | if padding_.locked then 195 | padding value 196 | 197 | else 198 | { padding_ | bottom = value } 199 | 200 | 201 | setPaddingLeft : Int -> Padding -> Padding 202 | setPaddingLeft value padding_ = 203 | if padding_.locked then 204 | padding value 205 | 206 | else 207 | { padding_ | left = value } 208 | 209 | 210 | 211 | -- TRANSFORMATION 212 | 213 | 214 | setOffsetX : Float -> { a | offsetX : Float } -> { a | offsetX : Float } 215 | setOffsetX value node = 216 | { node | offsetX = value } 217 | 218 | 219 | setOffsetY : Float -> { a | offsetY : Float } -> { a | offsetY : Float } 220 | setOffsetY value node = 221 | { node | offsetY = value } 222 | 223 | 224 | 225 | -- SPACING 226 | 227 | 228 | type Spacing 229 | = SpaceEvenly 230 | | Spacing ( Int, Int ) 231 | 232 | 233 | spacing : Int -> Spacing 234 | spacing value = 235 | Spacing ( value, value ) 236 | 237 | 238 | spacingXY : Int -> Int -> Spacing 239 | spacingXY x y = 240 | Spacing ( x, y ) 241 | 242 | 243 | setSpacing : Spacing -> { a | spacing : Spacing } -> { a | spacing : Spacing } 244 | setSpacing value node = 245 | { node | spacing = value } 246 | 247 | 248 | setSpacingX : Int -> Spacing -> Spacing 249 | setSpacingX value spacing_ = 250 | case spacing_ of 251 | Spacing ( _, y ) -> 252 | spacingXY value y 253 | 254 | SpaceEvenly -> 255 | spacing value 256 | 257 | 258 | setSpacingY : Int -> Spacing -> Spacing 259 | setSpacingY value spacing_ = 260 | case spacing_ of 261 | Spacing ( x, _ ) -> 262 | spacingXY x value 263 | 264 | SpaceEvenly -> 265 | spacing value 266 | 267 | 268 | 269 | -- MISC 270 | 271 | 272 | {-| Set lock flag for padding and borders. 273 | -} 274 | setLock : Bool -> { a | locked : Bool } -> { a | locked : Bool } 275 | setLock value record = 276 | { record | locked = value } 277 | -------------------------------------------------------------------------------- /src/Style/Shadow.elm: -------------------------------------------------------------------------------- 1 | module Style.Shadow exposing 2 | ( Shadow 3 | , ShadowType(..) 4 | , default 5 | , hasSize 6 | , isInner 7 | , isOuter 8 | , none 9 | , setBlur 10 | , setColor 11 | , setOffsetX 12 | , setOffsetY 13 | , setShadow 14 | , setSize 15 | , setType 16 | ) 17 | 18 | {-| Shadow properties. 19 | -} 20 | 21 | import Element exposing (Color) 22 | import Palette 23 | 24 | 25 | type ShadowType 26 | = Inner 27 | | Outer 28 | 29 | 30 | type alias Shadow = 31 | { offsetX : Float 32 | , offsetY : Float 33 | , size : Float 34 | , blur : Float 35 | , color : Color 36 | , type_ : ShadowType 37 | } 38 | 39 | 40 | none = 41 | Shadow 0 0 0 0 Palette.black Outer 42 | 43 | 44 | default = 45 | Shadow 0 5 0 15 Palette.darkGray Outer 46 | 47 | 48 | hasSize : Shadow -> Bool 49 | hasSize value = 50 | value.blur /= 0 || value.size /= 0 51 | 52 | 53 | isInner value = 54 | case value of 55 | Inner -> 56 | True 57 | 58 | _ -> 59 | False 60 | 61 | 62 | isOuter value = 63 | case value of 64 | Outer -> 65 | True 66 | 67 | _ -> 68 | False 69 | 70 | 71 | setShadow : Shadow -> { a | shadow : Shadow } -> { a | shadow : Shadow } 72 | setShadow value node = 73 | { node | shadow = value } 74 | 75 | 76 | setOffsetX : Float -> Shadow -> Shadow 77 | setOffsetX value record = 78 | { record | offsetX = value } 79 | 80 | 81 | setOffsetY : Float -> Shadow -> Shadow 82 | setOffsetY value record = 83 | { record | offsetY = value } 84 | 85 | 86 | setBlur : Float -> Shadow -> Shadow 87 | setBlur value record = 88 | { record | blur = value } 89 | 90 | 91 | setSize : Float -> Shadow -> Shadow 92 | setSize value record = 93 | { record | size = value } 94 | 95 | 96 | setColor : Color -> Shadow -> Shadow 97 | setColor value record = 98 | { record | color = value } 99 | 100 | 101 | setType : ShadowType -> Shadow -> Shadow 102 | setType value record = 103 | { record | type_ = value } 104 | -------------------------------------------------------------------------------- /src/Style/Theme.elm: -------------------------------------------------------------------------------- 1 | module Style.Theme exposing 2 | ( Theme 3 | , contrastColor 4 | , defaultTheme 5 | , large 6 | , regular 7 | , small 8 | , xlarge 9 | , xsmall 10 | ) 11 | 12 | import Element as E exposing (Color) 13 | import Fonts 14 | import Palette 15 | import Style.Border as Border exposing (..) 16 | import Style.Font as Font exposing (..) 17 | import Style.Layout as Layout exposing (..) 18 | 19 | 20 | type alias Theme = 21 | { textColor : Color 22 | , headingColor : Color 23 | , labelColor : Color 24 | , backgroundColor : Color 25 | , primaryColor : Color 26 | , accentColor : Color 27 | , mutedColor : Color 28 | , infoColor : Color 29 | , dangerColor : Color 30 | , warningColor : Color 31 | , successColor : Color 32 | , textFontFamily : FontFamily 33 | , headingFontFamily : FontFamily 34 | , textSize : Int 35 | , heading1Size : Int 36 | , heading2Size : Int 37 | , heading3Size : Int 38 | , textFontWeight : FontWeight 39 | , headingFontWeight : FontWeight 40 | , paragraphSpacing : Spacing 41 | , headingSpacing : Spacing 42 | , borderWidth : BorderWidth 43 | , borderColor : Color 44 | , borderCorner : BorderCorner 45 | } 46 | 47 | 48 | defaultTheme : Theme 49 | defaultTheme = 50 | { textColor = Palette.darkCharcoal 51 | , headingColor = Palette.darkCharcoal 52 | , labelColor = Palette.darkCharcoal 53 | , backgroundColor = Palette.white 54 | , primaryColor = Palette.blue 55 | , accentColor = Palette.orange 56 | , mutedColor = Palette.lightGray 57 | , infoColor = Palette.lightBlue 58 | , dangerColor = Palette.lightRed 59 | , warningColor = Palette.lightYellow 60 | , successColor = Palette.lightGreen 61 | , textSize = 16 62 | , heading1Size = 36 63 | , heading2Size = 24 64 | , heading3Size = 18 65 | , textFontFamily = Fonts.defaultFamily 66 | , headingFontFamily = Fonts.defaultFamily 67 | , textFontWeight = Regular 68 | , headingFontWeight = Bold 69 | , paragraphSpacing = Layout.spacingXY 0 (round (16 * 0.25)) 70 | , headingSpacing = Layout.spacing 0 71 | , borderWidth = Border.width 1 72 | , borderColor = Palette.darkGray 73 | , borderCorner = Border.corner 2 74 | } 75 | 76 | 77 | xsmall theme = 78 | theme.textSize // 4 79 | 80 | 81 | small theme = 82 | theme.textSize // 2 83 | 84 | 85 | regular theme = 86 | theme.textSize 87 | 88 | 89 | large theme = 90 | theme.textSize + theme.textSize // 2 91 | 92 | 93 | xlarge theme = 94 | theme.textSize * 3 95 | 96 | 97 | {-| See 98 | -} 99 | contrastColor color dark light = 100 | let 101 | { red, green, blue, alpha } = 102 | E.toRgb color 103 | in 104 | if ((red * 255 * 299) + (green * 255 * 587) + (blue * 255 * 114)) / 1000 > 150 then 105 | dark 106 | 107 | else 108 | light 109 | -------------------------------------------------------------------------------- /src/Views/Common.elm: -------------------------------------------------------------------------------- 1 | module Views.Common exposing 2 | ( addTooltipDown 3 | , addTooltipLeft 4 | , addTooltipRight 5 | , addTooltipUp 6 | , canDropInto 7 | , canDropNextTo 8 | , isDragging 9 | , none 10 | , widgetId 11 | ) 12 | 13 | import Document exposing (DragId(..)) 14 | import Html as H exposing (Attribute, Html) 15 | import Html.Attributes as A 16 | import Html5.DragDrop as DragDrop 17 | import Model exposing (Msg, Widget(..)) 18 | import Tree as T exposing (Tree) 19 | 20 | 21 | addTooltipUp = 22 | addTooltip "up" 23 | 24 | 25 | addTooltipDown = 26 | addTooltip "down" 27 | 28 | 29 | addTooltipLeft = 30 | addTooltip "left" 31 | 32 | 33 | addTooltipRight = 34 | addTooltip "right" 35 | 36 | 37 | addTooltip : String -> String -> List (Attribute Msg) -> List (Attribute Msg) 38 | addTooltip position text attrs = 39 | if String.isEmpty text then 40 | -- Do not create empty tooltip 41 | attrs 42 | 43 | else 44 | A.attribute "aria-label" text 45 | --:: A.attribute "data-balloon-length" "medium" 46 | :: A.attribute "data-balloon-pos" position 47 | :: attrs 48 | 49 | 50 | isDragging dragDrop = 51 | DragDrop.getDragId dragDrop /= Nothing 52 | 53 | 54 | canDropInto container dragDrop = 55 | case DragDrop.getDragId dragDrop of 56 | Just dragId -> 57 | case dragId of 58 | Move node -> 59 | Document.canInsertInto container node.type_ 60 | 61 | Drag node -> 62 | Document.canInsertInto container node.type_ 63 | 64 | Insert template -> 65 | Document.canInsertInto container (T.label template).type_ 66 | 67 | Nothing -> 68 | False 69 | 70 | 71 | canDropNextTo sibling dragDrop = 72 | case DragDrop.getDragId dragDrop of 73 | Just dragId -> 74 | case dragId of 75 | Move node -> 76 | -- Do not allow to drop an element sibling to itself 77 | (Document.nodeId sibling.id /= Document.nodeId node.id) && Document.canInsertNextTo sibling node.type_ 78 | 79 | Drag node -> 80 | Document.canInsertNextTo sibling node.type_ 81 | 82 | Insert template -> 83 | Document.canInsertNextTo sibling (T.label template).type_ 84 | 85 | Nothing -> 86 | False 87 | 88 | 89 | widgetId : Widget -> String 90 | widgetId field = 91 | case field of 92 | FontSizeField -> 93 | "font-size" 94 | 95 | FontColorField -> 96 | "font-color-hex" 97 | 98 | LetterSpacingField -> 99 | "letter-spacing" 100 | 101 | WordSpacingField -> 102 | "word-spacing" 103 | 104 | BackgroundColorField -> 105 | "background-color-hex" 106 | 107 | PaddingTopField -> 108 | "padding-top" 109 | 110 | PaddingRightField -> 111 | "padding-right" 112 | 113 | PaddingBottomField -> 114 | "padding-bottom" 115 | 116 | PaddingLeftField -> 117 | "padding-left" 118 | 119 | SpacingXField -> 120 | "spacing-x" 121 | 122 | SpacingYField -> 123 | "spacing-y" 124 | 125 | ImageSrcField -> 126 | "image-src" 127 | 128 | BackgroundImageField -> 129 | "background-image" 130 | 131 | BorderColorField -> 132 | "border-color-hex" 133 | 134 | BorderTopLeftCornerField -> 135 | "border-top-left-corner" 136 | 137 | BorderTopRightCornerField -> 138 | "border-top-right-corner" 139 | 140 | BorderBottomRightCornerField -> 141 | "border-bottom-right-corner" 142 | 143 | BorderBottomLeftCornerField -> 144 | "border-bottom-left-corner" 145 | 146 | BorderTopWidthField -> 147 | "border-top-width" 148 | 149 | BorderRightWidthField -> 150 | "border-right-width" 151 | 152 | BorderBottomWidthField -> 153 | "border-bottom-width" 154 | 155 | BorderLeftWidthField -> 156 | "border-left-width" 157 | 158 | LabelField -> 159 | "label" 160 | 161 | LabelColorField -> 162 | "label-color" 163 | 164 | OffsetXField -> 165 | "offset-x" 166 | 167 | OffsetYField -> 168 | "offset-y" 169 | 170 | WidthMinField -> 171 | "width-min" 172 | 173 | WidthMaxField -> 174 | "width-max" 175 | 176 | WidthPxField -> 177 | "width-px" 178 | 179 | WidthPortionField -> 180 | "width-portion" 181 | 182 | HeightMinField -> 183 | "height-min" 184 | 185 | HeightMaxField -> 186 | "height-max" 187 | 188 | HeightPxField -> 189 | "height-px" 190 | 191 | HeightPortionField -> 192 | "height-portion" 193 | 194 | ShadowOffsetXField -> 195 | "shadow-offset-x" 196 | 197 | ShadowOffsetYField -> 198 | "shadow-offset-y" 199 | 200 | ShadowSizeField -> 201 | "shadow-size" 202 | 203 | ShadowColorField -> 204 | "shadow-color-hex" 205 | 206 | ShadowBlurField -> 207 | "shadow-blur" 208 | 209 | InsertDropdown -> 210 | "insert" 211 | 212 | 213 | none = 214 | H.div [] [] 215 | -------------------------------------------------------------------------------- /src/Views/ContextMenuPopup.elm: -------------------------------------------------------------------------------- 1 | module Views.ContextMenuPopup exposing (view) 2 | 3 | import ContextMenu exposing (Config, ContextMenu, Cursor(..), Direction(..), Item, Overflow(..)) 4 | import Html as H exposing (Html) 5 | import Model exposing (..) 6 | 7 | 8 | view : ContextMenu ContextMenuPopup -> Html Msg 9 | view contextMenu = 10 | H.div 11 | [] 12 | [ ContextMenu.view 13 | contextMenuConfig 14 | ContextMenuMsg 15 | toItemGroups 16 | contextMenu 17 | ] 18 | 19 | 20 | toItemGroups : ContextMenuPopup -> List (List ( Item, Msg )) 21 | toItemGroups context = 22 | case context of 23 | -- Context menu items for outlive view 24 | OutlinePopup nodeId -> 25 | [ [ ( ContextMenu.item "Remove" |> ContextMenu.shortcut "Del", RemoveNodeClicked nodeId ) 26 | , ( ContextMenu.item "Duplicate", DuplicateNodeClicked nodeId ) 27 | ] 28 | , [ ( ContextMenu.item "Show in Workspace", NodeSelected True nodeId ) 29 | ] 30 | ] 31 | 32 | 33 | contextMenuConfig : Config 34 | contextMenuConfig = 35 | { width = 200 36 | , direction = RightBottom 37 | , overflowX = Mirror 38 | , overflowY = Mirror 39 | , containerColor = "white" 40 | , hoverColor = "#e9ecef" -- Gray 200 41 | , invertText = False 42 | , cursor = Pointer 43 | , rounded = True 44 | , fontFamily = "inherit" 45 | } 46 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Elm Designer 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |

Elm Designer is starting...

16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import { Elm } from "./Main.elm"; 2 | 3 | var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); 4 | var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); 5 | 6 | var seeds = new Uint32Array(4); 7 | window.crypto.getRandomValues(seeds); 8 | 9 | var app = Elm.Main.init({ 10 | flags: { 11 | width: w, 12 | height: h, 13 | seed1: seeds[0], 14 | seed2: seeds[1], 15 | seed3: seeds[2], 16 | seed4: seeds[3], 17 | platform: navigator.platform, 18 | }, 19 | node: document.getElementById("app"), 20 | }); 21 | 22 | // ------------------------------- 23 | // Simple localStorage support 24 | // ------------------------------- 25 | 26 | var storageKey = "lastDocument"; 27 | 28 | app.ports.saveDocument.subscribe(function (value) { 29 | localStorage.setItem(storageKey, value); 30 | }); 31 | 32 | app.ports.loadDocument.subscribe(function () { 33 | let value = localStorage.getItem(storageKey); 34 | // Sanity check for first run 35 | if (value) { 36 | app.ports.onDocumentLoad.send(value); 37 | } 38 | }); 39 | 40 | // ------------------------------- 41 | // Copy to clipboard 42 | // ------------------------------- 43 | 44 | app.ports.copyToClipboard.subscribe(function (value) { 45 | if (!navigator.clipboard) { 46 | return; 47 | } 48 | navigator.clipboard.writeText(value).then( 49 | function () {}, 50 | function (err) { 51 | console.error("Could not copy text to clipboard: ", err); 52 | } 53 | ); 54 | }); 55 | 56 | // ------------------------------- 57 | // Select text input/textarea 58 | // ------------------------------- 59 | 60 | app.ports.selectText.subscribe(function (id) { 61 | // We need to wait for Elm to render the