From 25850370dc6bce40c22ae91999fba277ee163245 Mon Sep 17 00:00:00 2001 From: Matthew Griffith Date: Tue, 28 Aug 2018 19:41:08 -0400 Subject: [PATCH] a stylish new design toolkit --- .gitignore | 13 +- CHANGES-FROM-STYLE-ELEMENTS.md | 202 ++ CSS-LOOKUP.md | 18 + LICENSE | 4 +- README.md | 57 + elm.json | 26 + examples/Basic.elm | 29 + examples/Form.elm | 201 ++ examples/Table.elm | 65 + examples/elm.json | 25 + experiments/Accumulator.elm | 57 + experiments/masks/Test.elm | 10 + experiments/masks/elm.json | 27 + experiments/masks/src/MaskedInput.elm | 130 + experiments/masks/src/Notes.elm | 189 ++ experiments/palette/PaletteTest.elm | 37 + experiments/palette/elm.json | 27 + experiments/palette/src/Palette.elm | 214 ++ src/Element.elm | 1587 ++++++++++++ src/Element/Background.elm | 232 ++ src/Element/Border.elm | 221 ++ src/Element/Events.elm | 272 ++ src/Element/Font.elm | 332 +++ src/Element/Input.elm | 1834 +++++++++++++ src/Element/Keyed.elm | 67 + src/Element/Lazy.elm | 117 + src/Element/Region.elm | 111 + src/Internal/Flag.elm | 260 ++ src/Internal/Grid.elm | 264 ++ src/Internal/Model.elm | 2847 +++++++++++++++++++++ src/Internal/Style.elm | 1628 ++++++++++++ tests/Tests/Basic.elm | 54 + tests/Tests/ColumnAlignment.elm | 176 ++ tests/Tests/ColumnSpacing.elm | 68 + tests/Tests/ElementAlignment.elm | 111 + tests/Tests/Manual/AlignedForm.elm | 286 +++ tests/Tests/Manual/All.elm | 1263 +++++++++ tests/Tests/Manual/LazyPerformance.elm | 523 ++++ tests/Tests/Manual/OtherScrollbars.elm | 36 + tests/Tests/Manual/Overflow.elm | 16 + tests/Tests/Manual/Scrollbars.elm | 62 + tests/Tests/Manual/ScrollbarsPls.elm | 58 + tests/Tests/Manual/TextLayout.elm | 43 + tests/Tests/Nearby.elm | 393 +++ tests/Tests/Palette.elm | 41 + tests/Tests/RowAlignment.elm | 212 ++ tests/Tests/RowSpacing.elm | 69 + tests/Tests/Run.elm | 28 + tests/Tests/Table.elm | 123 + tests/Tests/Transparency.elm | 57 + tests/Tests/WidthHeight.elm | 61 + tests/automation/selenium_test.py | 59 + tests/elm.json | 26 + tests/gather-styles.html | 83 + tests/live.sh | 1 + tests/src/Generator.elm | 53 + tests/src/Testable.elm | 730 ++++++ tests/src/Testable/Element.elm | 1045 ++++++++ tests/src/Testable/Element/Background.elm | 27 + tests/src/Testable/Element/Font.elm | 48 + tests/src/Testable/FuzzMain.elm | 629 +++++ tests/src/Testable/Runner.elm | 444 ++++ tests/suite/ClassNames.elm | 158 ++ tests/suite/Flags.elm | 89 + tests/suite/elm-package.json | 16 + 65 files changed, 18187 insertions(+), 4 deletions(-) create mode 100644 CHANGES-FROM-STYLE-ELEMENTS.md create mode 100644 CSS-LOOKUP.md create mode 100644 README.md create mode 100644 elm.json create mode 100644 examples/Basic.elm create mode 100644 examples/Form.elm create mode 100644 examples/Table.elm create mode 100644 examples/elm.json create mode 100644 experiments/Accumulator.elm create mode 100644 experiments/masks/Test.elm create mode 100644 experiments/masks/elm.json create mode 100644 experiments/masks/src/MaskedInput.elm create mode 100644 experiments/masks/src/Notes.elm create mode 100644 experiments/palette/PaletteTest.elm create mode 100644 experiments/palette/elm.json create mode 100644 experiments/palette/src/Palette.elm create mode 100644 src/Element.elm create mode 100644 src/Element/Background.elm create mode 100644 src/Element/Border.elm create mode 100644 src/Element/Events.elm create mode 100644 src/Element/Font.elm create mode 100644 src/Element/Input.elm create mode 100644 src/Element/Keyed.elm create mode 100644 src/Element/Lazy.elm create mode 100644 src/Element/Region.elm create mode 100644 src/Internal/Flag.elm create mode 100644 src/Internal/Grid.elm create mode 100644 src/Internal/Model.elm create mode 100644 src/Internal/Style.elm create mode 100644 tests/Tests/Basic.elm create mode 100644 tests/Tests/ColumnAlignment.elm create mode 100644 tests/Tests/ColumnSpacing.elm create mode 100644 tests/Tests/ElementAlignment.elm create mode 100644 tests/Tests/Manual/AlignedForm.elm create mode 100644 tests/Tests/Manual/All.elm create mode 100644 tests/Tests/Manual/LazyPerformance.elm create mode 100644 tests/Tests/Manual/OtherScrollbars.elm create mode 100644 tests/Tests/Manual/Overflow.elm create mode 100644 tests/Tests/Manual/Scrollbars.elm create mode 100644 tests/Tests/Manual/ScrollbarsPls.elm create mode 100644 tests/Tests/Manual/TextLayout.elm create mode 100644 tests/Tests/Nearby.elm create mode 100644 tests/Tests/Palette.elm create mode 100644 tests/Tests/RowAlignment.elm create mode 100644 tests/Tests/RowSpacing.elm create mode 100644 tests/Tests/Run.elm create mode 100644 tests/Tests/Table.elm create mode 100644 tests/Tests/Transparency.elm create mode 100644 tests/Tests/WidthHeight.elm create mode 100644 tests/automation/selenium_test.py create mode 100644 tests/elm.json create mode 100644 tests/gather-styles.html create mode 100644 tests/live.sh create mode 100644 tests/src/Generator.elm create mode 100644 tests/src/Testable.elm create mode 100644 tests/src/Testable/Element.elm create mode 100644 tests/src/Testable/Element/Background.elm create mode 100644 tests/src/Testable/Element/Font.elm create mode 100644 tests/src/Testable/FuzzMain.elm create mode 100644 tests/src/Testable/Runner.elm create mode 100644 tests/suite/ClassNames.elm create mode 100644 tests/suite/Flags.elm create mode 100644 tests/suite/elm-package.json diff --git a/.gitignore b/.gitignore index 8b631e7..ec022bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,15 @@ # elm-package generated files -elm-stuff +elm-stuff/ # elm-repl generated files repl-temp-* +.DS_Store +*/index.html +index.html +.elm-static-html + +tests/html-generation/rendered/ + +env +elm.js + +examples/temporary/ \ No newline at end of file diff --git a/CHANGES-FROM-STYLE-ELEMENTS.md b/CHANGES-FROM-STYLE-ELEMENTS.md new file mode 100644 index 0000000..ec88f39 --- /dev/null +++ b/CHANGES-FROM-STYLE-ELEMENTS.md @@ -0,0 +1,202 @@ +# Compared to Style Elements + + +This was a MAJOR rewrite of Style Elements. + + + +* **Major Performance improvement** - Style Elements v5 is much faster than v4 due to a better rendering strategy and generating very minimal html. The rewritten architecture also allows me to explore a few other optimizations, so things may get even faster than they are now. + +* **Lazy is here!** - It works with no weird caveats. + +## No Stylesheet + +You now define styles on the element itself. + +``` +el [ Background.color blue, Font.color white ] (text "I'm so stylish!") +``` + +These styles are gathered and rendered into a `stylesheet`. This has a few advantages: + +1. Much faster than rendering as actual inline styles +2. More expressive power by allowing style-elements compile to pseudoclasses, css animations and the like. +3. Defining styles like this is a really nice workflow. + +## Reorganization + +* No `Style` modules anymore, it's all under `Element`. +* `Style.Color` and `Style.Shadow` have been merged into `Element.Font`, `Element.Border`, and `Element.background`. So things like `Font.color` and `Border.glow` now exist. +* No more `Element.Attributes`, everything has been either moved to `Element` or to a more appropriate module. + + +## Wait isn't Style Elements about separation of layout and style?! + +It was! So why is there only `Element` and no `StyleSheet` now? + +The key insight is that it's not so much the separation of layout and style that is important as it is that _properties affecting layout should all be in the view function_. The main thing is _not_ having layout specified through several layers of indirection, but having everything explict and in one place. + +The new version moves to a more refined version of this idea: **Everything should be explicit and in your view!** + + +## Style Organization + +Your next question might be "if we don't have a stylesheet, how to we capture our style logic in a nice way?" + +The main thing I've found is that stylesheets in general seem to be pretty hard to maintain. Even well-typed stylesheets that only allow single classes! You have to manage names for everything, and in the case of large style refactors it's not obvious that style classes would be organized the same way. + +So, I'm not sure that stylesheets or something like stylesheets are the way to go. + +My current thinking is that you have 2 really powerful methods of capturing your styling logic. + +1. **Just put it in a view function.** If you have a few button variations you want, just create a function for each variation. You probably don't need a huge number of variations. You don't need to think of it like recreating bootstrap in style-elements. + +2. **Capture your colors, font sizes, etc.** Create a `Style` module that captures the **values** you use for your styling. Keep your colors there, as well as values for spacing, font names, and font sizes all in one place. You should consider using a scaling function for things like spacing and fontsizes. + + +## Element.Area + +The `Element.Area` module is now how you can do accessibility markup. + +You can do this by adding an area notation to an element's attributes. + +```elm +import Element.Area as Area + +row [ Area.navigation ] + [ --..my navigation links + ] + +``` +Or you can make something an `

` + +``` +el [ Area.heading 1 ] (text "Super important stuff") + +``` + +This means your accessibility markup is separate from your layout markup, which turns out to be really nice. + + + +## Alignment + +Alignment got a _bunch_ of attention to make it more powerful and intuitive. + +* _Alignment_ now applies to the element it's attached to, so Alignment on `row` and `column` does not apply to the children but to the `row` or `column` itself. + +* _It works everywhere!_ Previously, if you set a child as `alignLeft` in a row, nothing would happen. The main weirdness that had to be resolved is what happens to the other elements when an el in the middle of a row is aligned. The answer I came up with is that it will push other elements to the side. + +``` + alignLeft(pushes element on the left of it, to the left) + | center(is the default now) + | | alignRight + v v v + |-el-|-el-|---------|-el-|---------------|-el-| +``` + +Also of note, is that if something is `center`, then it will truly be in the center. + +* _Centered by Default_ - `el`s are centered by default. + + +## Things that have been removed/deprecated + + +**Percent** - `percent` is no longer there for width/height values. Just use `fill`, because you were probably just using `percent 100`, right? If you really need something like percent, you can manage it pretty easy with `fillPortion`. The main reason this goes away is that `percent` allows you to accidently overflow your element when trying to sum up multiple elements. + +**Grids** - `grid`, and `namedGrid` are gone. The reason for this is that for 95% of cases, just composing something with `row` and `column` results in _much_ nicer code. I'm open to see arguments for `grid`, but I need to see specific realworld usecases that can't be done using `row` and `column`. + +**WrappedRows/Columns** - `wrappedRow` and `wrappedColumn` are gone. From what I can see these cases don't show up very often. Removing them allows me to be cleaner with the internal style-elements code as well. + +**When/WhenJust** - `when` and `whenJust` are removed, though you can easily make a convenience function for yourself! I wanted the library to place an emphasis on common elm constructs instead of library-specific ones. As far as shortcuts, they don't actually save you much anyway. + +**Full, Spacer** - `full` and `spacer` have been removed in order to follow the libraries priority of explicitness. `full` would override the parents padding, while `spacer` would override the parent's `spacing`. Both can be achieved with the more common primities of `row`, `column` and `spacing`, and potentially some nesting of layouts. + + + +# New Version of the Alpha + +- `Font.weight` has been removed in favor of `Font.extraBold`, `Font.regular`, `Font.light`, etc. All weights from 100 - 900 are represented. +- `Background.image` and `Background.fittedImage` will place a centered background image, instead of anchoring at the top left. +- `fillBetween { min : Maybe Int, max : Maybe Int}` is now present for `min/max height/width` behavior. It works like fill, but with an optional top and lower bound. +- `transparent` - Set an element as transparent. It will take up space, but otherwise be transparent and unclickable. +- `alpha` can now be set for an element. +- `attribute` has been renamed `htmlAttribute` to better convey what it's used for. +- `Element.Area` has been renamed `Element.Region` to avoid confusion with `WAI ARIA` stuff. +- `center` has been renamed `centerX` + + + +# New Default Behavior + +The default logic has been made more consistent and hopefully more intuitive. + +Al elements start with `width/height shrink`, which means that they are the size of their contents. + + +# PseudoClass Support + +`Element.mouseOver`, `Element.focused`, and `Element.mouseDown` are available to style `:hover`, `:focus` and `:active`. + +Only a small subset of properties are allowed here or else the compiler will give you an error. + +This also introduced some new type aliases for attributes. + +`Attribute msg` - What you're used to. This **cannot** be used in a mouseOver/focused/etc. + +`Attr decorative msg` - A new attribute alias for attributes that can be used as a normal attribute or in `mouseOver`, `focused`, etc. I like to think of this as a *Decorative Attribute*. + + +# Input + +`Input.select` has been removed. Ultimately this came down to it being recommended against for most UX purposes. + +If you're looking for a replacement, consider any of these options which will likely create a better experience: + +- Input.checkbox +- Input.radio/Input.radioRow with custom styling +- Input.text with some sort of suggestion/autocomplete attached to it. + +If you still need to have a select menu, you can either: + +- *Embed one* using `html` +- [Craft one by having a hidden `radio` that is shown on focus.](https://gist.github.com/mdgriffith/b99b7ee04eaabaac042572e328a85345) You'll have to store some state that indicates if the menu is open or not, but you'd have to do that anyway if this library was directly supporting `select`. + +*Input.Notices* have been removed, which includes warnings and errors. Accessibility is important to this library and this change is actually meant to make it easier to have good form validation feedback. + +You can just use `above`/`below` when you need to show a validation message and it will be announced politely to users using screen readers. + +Notices were originally annotated as errors or warnings so that `aria-invalid` could be attached. However, it seems to me that having the changes be announced politely is better than having the screen reader just say "Yo, something's invalid". You now have more control over the feedback! Craft your messages well :) + + +Type aliases for the records used for inputs were also removed because it gives a nicer error message which references specific fields instead of the top level type alias. + + + +# New Testing Capabilities + +A test suite of ~1.6k layout tests was written(whew!). All of these tests pass on Chrome, Firefox, Safari, Edge, and IE11. + +# Overview of other changes + +- `Font.lineHeight` has been removed. Instead, `spacing` now works on paragraphs. +- `Element.empty` has been renamed `Element.none` to be more consistent with other elm libraries. +- `Device` no longer includes `window.width` and `window.height`. Previously every view function that depends on `device` was forced to rerender when the window was resized, which meant you couldn't take advantage of lazy. If you do need the window coordinates you can save them separately. +- *Fewer nodes rendered* - So, things should be faster! +- `fillBetween` has been replaced by `Element.minimum` and `Element.maximum`. + +So now you can do things like + +```elm +view = + el + [ width + (fill + |> minimum 20 + |> maximum 200 + ) + ] + (text "woohoo, I have a min and max") + +``` diff --git a/CSS-LOOKUP.md b/CSS-LOOKUP.md new file mode 100644 index 0000000..2db0b1d --- /dev/null +++ b/CSS-LOOKUP.md @@ -0,0 +1,18 @@ +# CSS Concepts and where to find them + +This library creates a new language around layout and style, though if you're already used to CSS, you're probably wondering where certain concepts lie. + +> I know how I can do it in CSS, but how could I approach the problem using Style Elements? + + +CSS | Style Elements | Description +-------|------------------|------------ +`position:absolute` | `above`, `below`, `onRight`, `onLeft`, `inFront`, `behindContent` | In Style Elements we can attach elements relative to another element. They won't affect normal flow, just like `position:absolute` +`position:fixed` | `inFront` if it's attached to the `Element.layout` element. | `position:fixed` needs to be at the top of your view or else it can break in seemingly random ways. Did you know `position:fixed` will position something relative to the viewport *OR* any parent that uses `filter`, `transform` or `perspective`? So you add a blur effect and your layout breaks... +`z-index` | __N/A__ | One of the goals of the library was to make `z-index` a behind-the-scenes detail. If you ever encounter a situation where you feel like you actually need it, let me know on slack or through the issues. +`float:left` `float:right` | `alignLeft` or `alignRight` when inside a `paragraph` or a `textColumn` | +`opacity` | `alpha` | +`margin` | __N/A__ Instead, check out `padding` and `spacing` | `margin` in CSS was designed to fight with `padding`. This library was designed to minimize override logic and properties that fight with each other in order to create a layout language that is predictable and easy to think about. The result is that in style elements, there's generally only *one place* where an effect can happen. +`:hover`, `:focus`, `:active` | `mouseOver`, `focused`, `mouseDown` | Only certain styles are allowed to be in a pseudo state. They have the type `Attr decorative msg`, which means they can be either an `Attribute` or a `Decoration`. +`
` | __N/a__ | __Elm__ already has a mechanism for submiting data to a server, namely the `Http` package. There has been some mention that the `form` element might be beneficial accessibility-wise, which I'm definitely open to hearing about! +`onSubmit` | __N/A__ | Similar to ``, there is no `onSubmit` behavior. Likely if you're attempting to capture some of the keybaord related behavior of `onSubmit`, you're likely better just crafting a keyboard even handler in the first place! diff --git a/LICENSE b/LICENSE index 5644d1e..2394149 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,3 @@ -BSD 3-Clause License - Copyright (c) 2018, Matthew Griffith All rights reserved. @@ -13,7 +11,7 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of the copyright holder nor the names of its +* Neither the name of Elm UI nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c234957 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# A New Language for Layout and Interface + +CSS and HTML are actually quite difficult to use when you're trying to do the layout and styling of a web page. + +This library is a complete alternative to HTML and CSS. Basically you can just write your app using this library and (mostly) never have to think about HTML and CSS again. + +The high level goal of this library is to be a **design toolkit** that draws inspiration from the domains of design, layout, and typography, as opposed to drawing from the ideas as implemented in CSS and HTML. + +This means: + +* Writing and designing your layout and `view` should be as **simple and as fun** as possible. +* Many layout errors (like you'd run into using CSS) **are just not possible to write** in the first place! +* Everything should just **run fast.** +* **Layout and style are explicit and easy to modify.** CSS and HTML as tools for a layout language are hard to modify because there's no central place that represents your layout. You're generally forced to bounce back and forth between multiple definitions in multiple files in order to adjust layout, even though it's probably the most common thing you'll do. + + +```elm +import Color exposing (blue, darkBlue) +import Element exposing (Element, el, text, row, alignRight) +import Element.Background as Background +import Element.Border as Border + + +main = + Element.layout [] + myElement + + +myRowOfStuff = + row [ width fill ] + [ myElement + , myElement + , el [ alignRight ] myElement + ] + + +myElement : Element msg +myElement = + el + [ Background.color blue + , Border.color darkBlue + ] + (text "You've made a stylish element!") +``` + + + + +## History + +The work is based off of a rewrite of the [Style Elements](https://github.com/mdgriffith/style-elements) library. A lot of that work was orignally released under the [Stylish Elephants](https://github.com/mdgriffith/stylish-elephants) project. + + + + + + diff --git a/elm.json b/elm.json new file mode 100644 index 0000000..8933bfa --- /dev/null +++ b/elm.json @@ -0,0 +1,26 @@ +{ + "type": "package", + "name": "mdgriffith/elm-ui", + "summary": "Layout and style that's easy to refactor, all without thinking about CSS.", + "license": "BSD-3-Clause", + "version": "1.0.0", + "exposed-modules": [ + "Element", + "Element.Input", + "Element.Events", + "Element.Background", + "Element.Border", + "Element.Font", + "Element.Lazy", + "Element.Keyed", + "Element.Region" + ], + "elm-version": "0.19.0 <= v < 0.20.0", + "dependencies": { + "elm/core": "1.0.0 <= v < 2.0.0", + "elm/html": "1.0.0 <= v < 2.0.0", + "elm/json": "1.0.0 <= v < 2.0.0", + "elm/virtual-dom": "1.0.0 <= v < 2.0.0" + }, + "test-dependencies": {} +} \ No newline at end of file diff --git a/examples/Basic.elm b/examples/Basic.elm new file mode 100644 index 0000000..3f71ac2 --- /dev/null +++ b/examples/Basic.elm @@ -0,0 +1,29 @@ +module Main exposing (..) + +{-| -} + +import Element exposing (..) +import Element.Background as Background +import Element.Font as Font +import Element.Input +import Element.Lazy + + +main = + Element.layout + [ Background.color (rgba 0 0 0 1) + , Font.color (rgba 1 1 1 1) + , Font.italic + , Font.size 32 + , Font.family + [ Font.external + { url = "https://fonts.googleapis.com/css?family=EB+Garamond" + , name = "EB Garamond" + } + , Font.sansSerif + ] + ] + <| + el + [ centerX, centerY ] + (text "Hello stylish friend!") diff --git a/examples/Form.elm b/examples/Form.elm new file mode 100644 index 0000000..dde096b --- /dev/null +++ b/examples/Form.elm @@ -0,0 +1,201 @@ +module Main exposing (..) + +{-| -} + +import Browser +import Element exposing (..) +import Element.Background as Background +import Element.Border as Border +import Element.Font as Font +import Element.Input as Input +import Element.Region as Region + + +white = + Element.rgb 1 1 1 + + +grey = + Element.rgb 0.9 0.9 0.9 + + +blue = + Element.rgb 0 0 0.8 + + +red = + Element.rgb 0.8 0 0 + + +darkBlue = + Element.rgb 0 0 0.9 + + +main = + Browser.sandbox + { init = init + , view = view + , update = update + } + + +init = + { username = "" + , password = "" + , agreeTOS = False + , comment = "Extra hot sauce?\n\n\nYes pls" + , lunch = Gyro + , spiciness = 2 + } + + +type alias Form = + { username : String + , password : String + , agreeTOS : Bool + , comment : String + , lunch : Lunch + , spiciness : Float + } + + +type Msg + = Update Form + + +update msg model = + case Debug.log "msg" msg of + Update new -> + new + + +type Lunch + = Burrito + | Taco + | Gyro + + +view model = + Element.layout + [ Font.size 20 + ] + <| + Element.column [ width (px 800), height shrink, centerY, centerX, spacing 36, padding 10, explain Debug.todo ] + [ el + [ Region.heading 1 + , alignLeft + , Font.size 36 + ] + (text "Welcome to the Stylish Elephants Lunch Emporium") + , Input.radio + [ spacing 12 + , Background.color grey + ] + { selected = Just model.lunch + , onChange = \new -> Update { model | lunch = new } + , label = Input.labelAbove [ Font.size 14, paddingXY 0 12 ] (text "What would you like for lunch?") + , options = + [ Input.option Gyro (text "Gyro") + , Input.option Burrito (text "Burrito") + , Input.option Taco (text "Taco") + ] + } + , Input.username + [ spacing 12 + , below + (el + [ Font.color red + , Font.size 14 + , alignRight + , moveDown 6 + ] + (text "This one is wrong") + ) + ] + { text = model.username + , placeholder = Just (Input.placeholder [] (text "username")) + , onChange = \new -> Update { model | username = new } + , label = Input.labelAbove [ Font.size 14 ] (text "Username") + } + , Input.currentPassword [ spacing 12, width shrink ] + { text = model.password + , placeholder = Nothing + , onChange = \new -> Update { model | password = new } + , label = Input.labelAbove [ Font.size 14 ] (text "Password") + , show = False + } + , Input.multiline + [ height shrink + , spacing 12 + + -- , padding 6 + ] + { text = model.comment + , placeholder = Just (Input.placeholder [] (text "Extra hot sauce?\n\n\nYes pls")) + , onChange = \new -> Update { model | comment = new } + , label = Input.labelAbove [ Font.size 14 ] (text "Leave a comment!") + , spellcheck = False + } + , Input.checkbox [] + { checked = model.agreeTOS + , onChange = \new -> Update { model | agreeTOS = new } + , icon = Input.defaultCheckbox + , label = Input.labelRight [] (text "Agree to Terms of Service") + } + , Input.slider + [ Element.height (Element.px 30) + , Element.behindContent + (Element.el + [ Element.width Element.fill + , Element.height (Element.px 2) + , Element.centerY + , Background.color grey + , Border.rounded 2 + ] + Element.none + ) + ] + { onChange = \new -> Update { model | spiciness = new } + , label = Input.labelAbove [] (text ("Spiciness: " ++ String.fromFloat model.spiciness)) + , min = 0 + , max = 3.2 + , step = Nothing + , value = model.spiciness + , thumb = + Input.defaultThumb + } + , Input.slider + [ Element.width (Element.px 40) + , Element.height (Element.px 200) + , Element.behindContent + (Element.el + [ Element.height Element.fill + , Element.width (Element.px 2) + , Element.centerX + , Background.color grey + , Border.rounded 2 + ] + Element.none + ) + ] + { onChange = \new -> Update { model | spiciness = new } + , label = Input.labelAbove [] (text ("Spiciness: " ++ String.fromFloat model.spiciness)) + , min = 0 + , max = 3.2 + , step = Nothing + , value = model.spiciness + , thumb = + Input.defaultThumb + } + , Input.button + [ Background.color blue + , Font.color white + , Border.color darkBlue + , paddingXY 32 16 + , Border.rounded 3 + , width fill + ] + { onPress = Nothing + , label = Element.text "Place your lunch order!" + } + ] diff --git a/examples/Table.elm b/examples/Table.elm new file mode 100644 index 0000000..4290536 --- /dev/null +++ b/examples/Table.elm @@ -0,0 +1,65 @@ +module Main exposing (..) + +{-| -} + +import Element exposing (..) +import Element.Background as Background +import Element.Font as Font +import Element.Input +import Element.Lazy + + +type alias Person = + { firstName : String + , lastName : String + } + + +persons : List Person +persons = + [ { firstName = "David" + , lastName = "Bowie" + } + , { firstName = "Florence" + , lastName = "Welch" + } + ] + + +main = + Element.layout + [ Background.color (rgba 0 0 0 1) + , Font.color (rgba 1 1 1 1) + , Font.italic + , Font.size 32 + , Font.family + [ Font.external + { url = "https://fonts.googleapis.com/css?family=EB+Garamond" + , name = "EB Garamond" + } + , Font.sansSerif + ] + ] + <| + Element.table + [ Element.centerX + , Element.centerY + , Element.spacing 5 + , Element.padding 10 + ] + { data = persons + , columns = + [ { header = Element.text "First Name" + , width = px 200 + , view = + \person -> + Element.text person.firstName + } + , { header = Element.text "Last Name" + , width = fill + , view = + \person -> + Element.text person.lastName + } + ] + } diff --git a/examples/elm.json b/examples/elm.json new file mode 100644 index 0000000..b77f68d --- /dev/null +++ b/examples/elm.json @@ -0,0 +1,25 @@ +{ + "type": "application", + "source-directories": [ + ".", + "../src/" + ], + "elm-version": "0.19.0", + "dependencies": { + "direct": { + "elm/browser": "1.0.0", + "elm/core": "1.0.0", + "elm/html": "1.0.0", + "elm/json": "1.0.0", + "elm/virtual-dom": "1.0.0" + }, + "indirect": { + "elm/time": "1.0.0", + "elm/url": "1.0.0" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} \ No newline at end of file diff --git a/experiments/Accumulator.elm b/experiments/Accumulator.elm new file mode 100644 index 0000000..1771f23 --- /dev/null +++ b/experiments/Accumulator.elm @@ -0,0 +1,57 @@ +module Accumulation exposing (..) + +{-| -} + +import Set + + +type Style + = Color String Float Float Float Float + | Spacing String Float + | Font String (List String) + + +key style = + case style of + Color k _ _ _ _ -> + k + + Spacing k _ -> + k + + Font k _ -> + k + + +type Element + = Element (List Style) (List Element) + | None + + +render element = + case element of + None -> + [] + + Element styles children -> + let + childrenStyles = + List.foldr render [] children + in + styles ++ childrenStyles + + +finalize styles = + List.foldl deduplicate ( Set.empty, [] ) styles + + +deduplicate style ( cached, styles ) = + if Set.member (key style) then + ( cached, styles ) + else + ( Set.insert (key style), style :: styles ) + + +toHtml element = + render element + |> finalize diff --git a/experiments/masks/Test.elm b/experiments/masks/Test.elm new file mode 100644 index 0000000..1838aab --- /dev/null +++ b/experiments/masks/Test.elm @@ -0,0 +1,10 @@ +module Main exposing (..) + +{-| -} + +import Html +import MaskedInput + + +main = + Html.text "Testing" diff --git a/experiments/masks/elm.json b/experiments/masks/elm.json new file mode 100644 index 0000000..d0b3f06 --- /dev/null +++ b/experiments/masks/elm.json @@ -0,0 +1,27 @@ +{ + "type": "application", + "source-directories": [ + ".", + "src" + ], + "elm-version": "0.19.0", + "dependencies": { + "direct": { + "elm/browser": "1.0.0", + "elm/core": "1.0.0", + "elm/html": "1.0.0", + "elm/json": "1.0.0", + "elm/parser": "1.0.0", + "elm/virtual-dom": "1.0.0", + "elm-community/list-extra": "8.0.0" + }, + "indirect": { + "elm/time": "1.0.0", + "elm/url": "1.0.0" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} \ No newline at end of file diff --git a/experiments/masks/src/MaskedInput.elm b/experiments/masks/src/MaskedInput.elm new file mode 100644 index 0000000..4af96ae --- /dev/null +++ b/experiments/masks/src/MaskedInput.elm @@ -0,0 +1,130 @@ +module MaskedInput exposing (..) + +{-| When we've created a Mask, it should be able to: + + - The validator's job is only to say if an entire string is valid. + - If a string is valid, then a message is sent out with the updated value. + - If a string is not valid, then the last(i.e. the current) value is sent out on a message. + - We have to send out a message on every input or else we'll get out of sync.(?) + + - The formatter's job is to take an input string and to format it into a view-string + + - The capturing parser's job is to transform the input string into the desired value. + - If the parser fails, we keep returning `Partial` + - As soon as it succeeds, we can return `Full`, which can have a value extracted. + +First pass at full description. + +Mask is defined in a view. + + - An initial string is potentially given. + - String is formatted and displayed + - onInput handler is registered which parses the value, and creates either a Partial or a Full + - The onInput handler will return the formatted string. + ->? we could diff it against a previous formatted string to see what changed. Maybe too complicated + ->? Capture keyboard events directly and make modifications there. What about pasting? + ->! Have the parser operate directly on the formatted string. + +-} + +import Parser exposing (Parser) + + +type Masked input + = Mask + { capture : Parser input -- String -> input + , format : List Formatter -- tel: "86" -> "(86 ) - " + , validate : List Validator -- Is a character allowed to be typed + } + + +type Validator + = Validator Int (Char -> Bool) + + +type Formatter + = Exactly String + | FromInput Int (Maybe Hint) -- hint is shown if no string can be retrieved + + +type alias Hint = + String + + +type Captured thing + = Partial String + | Full thing String + + +capture : input -> Masked input +capture value = + Mask + { capture = Parser.succeed value + , format = [] + , validate = [] + } + + +{-| Shows a static string. +-} +show str (Mask mask) = + Mask + { mask + | format = mask.format ++ [ Exactly str ] + , capture = Parser.token str + } + + +type alias Match = + { length : Int + , valid : Char -> Bool + , hint : Maybe String + } + + +{-| -} +match matcher (Mask mask) = + Mask + { mask + | format = mask.format ++ [ FromInput matcher.length matcher.hint ] + , validate = mask.validate ++ [ Validator matcher.length matcher.valid ] + , capture = Parser.exactly matcher.length matcher.valid + } + + + +-- map : (a -> b) -> Masked a -> Masked b +-- map = +-- Debug.crash "TODO" +-- map2 : +-- (a -> b -> value) +-- -> Masked a +-- -> Masked b +-- -> Masked value +-- map2 = +-- Debug.crash "TODO" + + +view : String -> Masked input -> Element msg +view = + Debug.crash + + +captureValue : String -> Masked input -> Captured input +captureValue input (Mask mask) = + case Parser.run mask.capture input of + Ok val -> + Full val input + + Err _ -> + Partial input + + +valid : Captured input -> Maybe input +valid cap = + case cap of + Partial _ -> + Nothing + + Full result _ -> + Just result diff --git a/experiments/masks/src/Notes.elm b/experiments/masks/src/Notes.elm new file mode 100644 index 0000000..5c67d78 --- /dev/null +++ b/experiments/masks/src/Notes.elm @@ -0,0 +1,189 @@ +module Element.Input.Mask exposing (..) + +{-| With masked input we can simultaneously descibe: + + - a parser to only allow specific arguments + - We have a set of expected characters and how many of them to expect. + - Static formating. Meaning show a `/` between two numbers, but it's not part of the input + - Hints. These are mini placeholder values for a section of your input. + - Autocomplete suggestions. These are suggestions that can be selected and completed with `tab`. + +-} + + +type Masked input + = Mask (List Pattern) + + +type Pattern + = Capture Limit (Char -> Bool) + | Decimal + | Decoration String + + +type Limit + = NoLimit + | Min Int + | Max Int + | MinMax Int Int + + +type Input thing + = Partial String + | Full thing String + + +type alias CreditCard = + { number : String + , expMonth : String + , expYear : String + , ccv : String + } + + +type alias CreditCardNumber = + String + + +example = + capture (\one two three four -> CreditCardNumber (one ++ two ++ three ++ four)) + |> Mask.stringWith + { length = 4 + , valid = String.isDigit + , hint = "1234" + } + |> Mask.show " " + |> Mask.stringWith + { length = 4 + , valid = String.isDigit + , hint = "1234" + } + |> Mask.show " " + |> Mask.stringWith + { length = 4 + , valid = String.isDigit + , hint = "1234" + } + |> Mask.space + |> Mask.stringWith + { length = 4 + , valid = String.isDigit + , hint = "1234" + } + |> Mask.andThen + (\creditcard -> + capture (CreditCard creditNumber) + |> Mask.show "1234" + -- last four digits of credit card + |> Mask.space + |> Mask.stringWith + { length = 2 + , valid = String.isDigit + , hint = "MM" + } + |> Mask.show "/" + |> Mask.stringWith + { length = 2 + , valid = String.isDigit + , hint = "YY" + } + |> Mask.space + |> Mask.stringWith + { length = 4 + , valid = String.isDigit + , hint = "CCV" + } + ) + + +float = + Mask.int + |> Mask.token "." + |> Mask.andThen + (\one -> + masked (\two -> one + two) + --combine ints in a way + |> Mask.token (toString one) + |> Mask.token "." + |> Mask.stringWith + { length = 3 -- No length restrictions + , valid = String.isDigit + , hint = "CCV" + } + ) + + +{-| + + + + + +-} + + + +-- type Masked input +-- = Mask +-- { capture : Parser input -- String -> input +-- , format : List Formatter -- tel: "86" -> "(86 ) - " +-- , validate : List Validator -- Is a character allowed to be typed +-- } +-- type Validator +-- = Match Int (Char -> Bool) +-- type Formatter +-- = Exactly String +-- | FromInput Int Hint -- hint is shown if no string can be retrieved +-- type alias Hint = +-- String +-- -- type Masked input = +-- -- { parser : Parser input +-- -- , +-- -- } +-- {-| We want this to be in +-- ---> formatting +-- -x-> result +-- -} +-- show str mask = +-- mask +-- {-| ---> formatting +-- ---> result +-- | if nothing's parsed +-- hint ---> formatting +-- hint -x-> result +-- -} +-- stringWith options mask = +-- mask +-- type Pattern input +-- = Capture Limit (Char -> Bool) +-- | Decimal +-- | Show String +-- type Limit +-- = NoLimit +-- | Min Int +-- | Max Int +-- | MinMax Int Int +-- type Captured input +-- = Partial String +-- | Full input String +-- {-| A placeholder ot represent different pieces having different styles +-- -} +-- type Styled +-- = Styled String String +-- render : String -> Masked input -> List Styled +-- captureValue : String -> Masked input -> Captured input +-- capture : input -> Masked input +-- map : (a -> b) -> Masked a -> Masked b +-- map2 : +-- (a -> b -> value) +-- -> Masked a +-- -> Masked b +-- -> Masked value +-- andThen : (a -> Masked b) -> Masked a -> Masked b +-- valid : Captured input -> Maybe input +-- valid cap = +-- case cap of +-- Partial _ -> +-- Nothing +-- Full result _ -> +-- Just result diff --git a/experiments/palette/PaletteTest.elm b/experiments/palette/PaletteTest.elm new file mode 100644 index 0000000..0ba6995 --- /dev/null +++ b/experiments/palette/PaletteTest.elm @@ -0,0 +1,37 @@ +module Main exposing (..) + +import Element exposing (..) +import Html +import Palette + + +{- First, Define our Palette + + Usually this would be in a different file. + +-} + + +type alias MyColorPalette = + { primary : Palette.Protected Color + , secondary : Palette.Protected Color + } + +myColors : Palette.Colors MyColorPalette +myColors = + Palette.colors MyColorPalette + |> Palette.color (rgb 1 0 0) + |> Palette.color (rgb 1 0 1) + +main = + Palette.layout myColors + [] + (Palette.Element + [ + -- Palette.bgColor red + -- + Palette.bgColor .primary + -- Palette.bgColor (Palette.dynamic (rgb 0 1 0)) + ] + [] + ) diff --git a/experiments/palette/elm.json b/experiments/palette/elm.json new file mode 100644 index 0000000..bbda846 --- /dev/null +++ b/experiments/palette/elm.json @@ -0,0 +1,27 @@ +{ + "type": "application", + "source-directories": [ + ".", + "src", + "../src/" + ], + "elm-version": "0.19.0", + "dependencies": { + "direct": { + "elm/browser": "1.0.0", + "elm/core": "1.0.0", + "elm/html": "1.0.0", + "elm/json": "1.0.0", + "elm/virtual-dom": "1.0.0", + "elm-community/list-extra": "8.0.0" + }, + "indirect": { + "elm/time": "1.0.0", + "elm/url": "1.0.0" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} \ No newline at end of file diff --git a/experiments/palette/src/Palette.elm b/experiments/palette/src/Palette.elm new file mode 100644 index 0000000..3d57090 --- /dev/null +++ b/experiments/palette/src/Palette.elm @@ -0,0 +1,214 @@ +module Palette exposing (..) + +{-| With palettes, we want the dev to be able to + + - specify some colors + - those colors then need to be rendered by the layout + - those colors can then be referred to by name in the layout + - A lookup is done to retrieve the classname + +-} + +import Element exposing (Color) +import Html exposing (Html) +import Html.Attributes +import Internal.Model as Internal + + +{- Declare a Palette of Colors (or any value, but colors for not) -} +{- -} + + +layout : Colors colors -> List (Attribute colors msg) -> Element colors msg -> Html msg +layout colorPalette attrs child = + -- Render a "stylesheet" + let + ( html, dynamicStyles ) = + renderPaletteElement colorPalette (Element attrs [ child ]) + in + Html.div [ Html.Attributes.class "root" ] + [ Html.text "static stylesheet" + , Html.div + [ Html.Attributes.style "padding" "20px" + , Html.Attributes.style "whitespace" "pre" + , Html.Attributes.style "border-radius" "4px" + , Html.Attributes.style "background-color" "#CCDDCC" + , Html.Attributes.style "font-family" "Open Sans" + ] + [ Html.text (String.join "\n" (renderColorPalette colorPalette)) ] + , Html.node "style" + [] + [ Html.text ("html,body,.root {width: 100%; height: 100%;}.ui {min-width:100px; min-height: 100px;}" ++ String.join "\n" (renderColorPalette colorPalette)) + ] + , Html.text "dynamic stylesheet" + , Html.div + [ Html.Attributes.style "padding" "20px" + , Html.Attributes.style "whitespace" "pre" + , Html.Attributes.style "border-radius" "4px" + , Html.Attributes.style "background-color" "#FFDDCC" + , Html.Attributes.style "font-family" "Open Sans" + ] + [ Html.text (String.join "\n" dynamicStyles) ] + , Html.node "style" [] [ Html.text (String.join "\n" dynamicStyles) ] + , Html.text "content:" + , html + ] + + +renderColor clr = + Internal.formatColor clr + + +colorRule clr = + ".bg-clr-" + ++ Internal.formatColorClass clr + ++ "{ background-color:" + ++ Internal.formatColor clr + ++ " }" + + +renderColorPalette : Colors colors -> List String +renderColorPalette (Colors palette) = + List.map + colorRule + palette.values + + +renderPaletteElement : Colors colors -> InternalElement (colors -> Protected Color) msg -> ( Html msg, List String ) +renderPaletteElement colorPalette (Element attrs children) = + let + gatherAttr attr ( attributes, myAttrStyles ) = + let + ( cls, style ) = + renderPaletteAttribute colorPalette attr + in + ( cls ++ " " ++ attributes, style :: myAttrStyles ) + + ( classes, attrStyles ) = + List.foldl gatherAttr ( "", [] ) attrs + + ( renderedChildren, styles ) = + List.foldr gather ( [], attrStyles ) children + + gather child ( rendered, existingStyles ) = + let + ( childHtml, childStyles ) = + renderPaletteElement colorPalette child + in + ( childHtml :: rendered, childStyles ++ existingStyles ) + in + ( Html.div [ Html.Attributes.class ("ui " ++ classes) ] + renderedChildren + , styles + ) + + +renderPaletteAttribute : Colors colors -> InternalAttribute (colors -> Protected Color) msg -> ( String, String ) +renderPaletteAttribute (Colors palette) attribute = + case attribute of + InternalAttribute -> + ( "ui", "" ) + + ColorStyle colorLookup -> + case colorLookup palette.protected of + Dynamic clr -> + ( "bg-clr-" ++ Internal.formatColorClass clr, colorRule clr ) + + Protected clr -> + ( "bg-clr-" ++ Internal.formatColorClass clr, "" ) + + +renderAttribute : Colors color -> InternalAttribute Color msg -> String +renderAttribute (Colors palette) attribute = + case attribute of + InternalAttribute -> + "" + + ColorStyle clr -> + "dynamic clr" + + +bgColor : color -> InternalAttribute color msg +bgColor clr = + ColorStyle clr + + +{-| Concrete Values +-} +type alias Attr msg = + InternalAttribute Color msg + + +{-| Palette based attributes +-} +type alias Attribute colors msg = + InternalAttribute (colors -> Protected Color) msg + + +type alias Element colors msg = + InternalElement (colors -> Protected Color) msg + + +type alias El msg = + InternalElement Color msg + + +type InternalAttribute color msg + = InternalAttribute + | ColorStyle color + + +type InternalElement color msg + = Element (List (InternalAttribute color msg)) (List (InternalElement color msg)) + + +{-| This is how we keep track of something that's already rendered, (Protected), or needs to be rendered +-} +type Protected thing + = Protected thing + | Dynamic thing + + + +-- dynamic : Color -> Colors colors -> Protected Color + + +dynamic value palette = + Dynamic value + + + +{- COLORS -} + + +type Colors a + = Colors + { protected : a + , values : List Color + } + + +colors : a -> Colors a +colors a = + Colors + { protected = a + , values = [] + } + + +color : Color -> Colors (Protected Color -> a) -> Colors a +color clr pal = + let + addColor p = + { protected = p.protected (Protected clr) + , values = clr :: p.values + } + in + map addColor pal + + +map : ({ protected : a, values : List Color } -> { protected : a1, values : List Color }) -> Colors a -> Colors a1 +map fn pal = + case pal of + Colors a -> + Colors (fn a) diff --git a/src/Element.elm b/src/Element.elm new file mode 100644 index 0000000..c6fbac7 --- /dev/null +++ b/src/Element.elm @@ -0,0 +1,1587 @@ +module Element exposing + ( Element, none, text, el + , row, wrappedRow, column + , paragraph, textColumn + , Column, table, IndexedColumn, indexedTable + , Attribute, width, height, Length, px, shrink, fill, fillPortion, maximum, minimum + , explain + , padding, paddingXY, paddingEach + , spacing, spacingXY, spaceEvenly + , centerX, centerY, alignLeft, alignRight, alignTop, alignBottom + , transparent, alpha, pointer + , moveUp, moveDown, moveRight, moveLeft, rotate, scale + , clip, clipX, clipY + , scrollbars, scrollbarX, scrollbarY + , layout, layoutWith, Option, noStaticStyleSheet, forceHover, noHover, focusStyle, FocusStyle + , link, newTabLink, download, downloadAs + , image + , Color, rgba, rgb, rgb255, rgba255, fromRgb, fromRgb255, toRgb + , above, below, onRight, onLeft, inFront, behindContent + , Attr, Decoration, mouseOver, mouseDown, focused + , Device, DeviceClass(..), Orientation(..), classifyDevice + , modular + , map, mapAttribute + , html, htmlAttribute + ) + +{-| + + +## Basic Elements + +@docs Element, none, text, el + + +## Rows and Columns + +Rows and columns are the most common layouts. + +@docs row, wrappedRow, column + + +## Text Layout + +Text needs it's own layout primitives. + +@docs paragraph, textColumn + + +## Data Table + +@docs Column, table, IndexedColumn, indexedTable + + +## Size + +@docs Attribute, width, height, Length, px, shrink, fill, fillPortion, maximum, minimum + + +## Debugging + +@docs explain + + +## Padding and Spacing + +There's no concept of margin in `style-elements`, instead we have padding and spacing. + +Padding is the distance between the outer edge and the content, and spacing is the space between children. + +So, if we have the following row, with some padding and spacing. + + Element.row [ padding 10, spacing 7 ] + [ Element.el [] none + , Element.el [] none + , Element.el [] none + ] + +Here's what we can expect: + +![Three boxes spaced 7 pixels apart. There's a 10 pixel distance from the edge of the parent to the boxes.](https://mdgriffith.gitbooks.io/style-elements/content/assets/spacing-400.png) + +@docs padding, paddingXY, paddingEach + +@docs spacing, spacingXY, spaceEvenly + + +## Alignment + +Alignment can be used to align an `Element` within another `Element`. + + Element.el [ centerX, alignTop ] (text "I'm centered and aligned top!") + +If alignment is set on elements in a layout such as `row`, then the element will push the other elements in that direction. Here's an example. + + Element.row [] + [ Element.el [] Element.none + , Element.el [ alignLeft ] Element.none + , Element.el [ centerX ] Element.none + , Element.el [ alignRight ] Element.none + ] + +will result in a layout like + + |-|-| |-| |-| + +Where there are two elements on the left, one in the center, and one on the right. + +@docs centerX, centerY, alignLeft, alignRight, alignTop, alignBottom + + +# Transparency + +@docs transparent, alpha, pointer + + +# Adjustment + +@docs moveUp, moveDown, moveRight, moveLeft, rotate, scale + + +# Clipping and Scrollbars + +Clip the content if it overflows. + +@docs clip, clipX, clipY + +Add a scrollbar if the content is larger than the element. + +@docs scrollbars, scrollbarX, scrollbarY + + +# Rendering + +@docs layout, layoutWith, Option, noStaticStyleSheet, forceHover, noHover, focusStyle, FocusStyle + + +# Links + +@docs link, newTabLink, download, downloadAs + + +# Images + +@docs image + + +# Color + +In order to use attributes like `Font.color` and `Background.color`, you'll need to make some colors! + +@docs Color, rgba, rgb, rgb255, rgba255, fromRgb, fromRgb255, toRgb + + +# Nearby Elements + +Let's say we want a dropdown menu. Essentially we want to say: _put this element below this other element, but don't affect the layout when you do_. + + Element.row [] + [ Element.el + [ Element.below (Element.text "I'm below!") + ] + (Element.text "I'm normal!") + ] + +This will result in + + |- I'm normal! -| + I'm below + +Where `"I'm Below"` doesn't change the size of `Element.row`. + +This is very useful for things like dropdown menus or tooltips. + +@docs above, below, onRight, onLeft, inFront, behindContent + + +# Temporary Styling + +@docs Attr, Decoration, mouseOver, mouseDown, focused + + +# Responsiveness + +The main technique for responsiveness is to store window size information in your model. + +Install the `Browser` package, and set up a subscription for [`Browser.Events.onResize`](https://package.elm-lang.org/packages/elm/browser/latest/Browser-Events#onResize). + +You'll also need to retrieve the initial window size. You can either run the following task: + + Task.perform (\size -> WindowResize { width = size.width, height = size.height }) Window.size + +Or pass in `window.innerWidth` and `window.innerHeight` as flags to your program, which is the preferred way. This requires minor setup on the JS side, but allows you to avoid the state where you don't have window info. + +@docs Device, DeviceClass, Orientation, classifyDevice + + +# Scaling + +@docs modular + + +## Mapping + +@docs map, mapAttribute + + +## Compatibility + +@docs html, htmlAttribute + +-} + +import Html exposing (Html) +import Html.Attributes +import Internal.Flag as Flag exposing (Flag) +import Internal.Model as Internal +import Internal.Style exposing (classes) + + +{-| -} +type alias Color = + Internal.Color + + +{-| Provide the red, green, and blue channels for the color. + +Each channel takes a value between 0 and 1. + +-} +rgb : Float -> Float -> Float -> Color +rgb r g b = + Internal.Rgba r g b 1 + + +{-| -} +rgba : Float -> Float -> Float -> Float -> Color +rgba = + Internal.Rgba + + +{-| Provide the red, green, and blue channels for the color. + +Each channel takes a value between 0 and 1. + +-} +rgb255 : Int -> Int -> Int -> Color +rgb255 red green blue = + Internal.Rgba + (toFloat red / 255) + (toFloat green / 255) + (toFloat blue / 255) + 1 + + +{-| -} +rgba255 : Int -> Int -> Int -> Float -> Color +rgba255 red green blue a = + Internal.Rgba + (toFloat red / 255) + (toFloat green / 255) + (toFloat blue / 255) + a + + +{-| Create a color from an RGB record. +-} +fromRgb : + { red : Float + , green : Float + , blue : Float + , alpha : Float + } + -> Color +fromRgb clr = + Internal.Rgba + clr.red + clr.green + clr.blue + clr.alpha + + +{-| -} +fromRgb255 : + { red : Int + , green : Int + , blue : Int + , alpha : Float + } + -> Color +fromRgb255 clr = + Internal.Rgba + (toFloat clr.red / 255) + (toFloat clr.green / 255) + (toFloat clr.blue / 255) + clr.alpha + + +{-| Deconstruct a `Color` into it's rgb channels. +-} +toRgb : + Color + -> + { red : Float + , green : Float + , blue : Float + , alpha : Float + } +toRgb (Internal.Rgba r g b a) = + { red = r + , green = g + , blue = b + , alpha = a + } + + +{-| The basic building block of your layout. Here we create a + + import Element + + view = + Element.el [] (Element.text "Hello!") + +-} +type alias Element msg = + Internal.Element msg + + +{-| Standard attribute which cannot be a decoration. +-} +type alias Attribute msg = + Internal.Attribute () msg + + +{-| This is a special attribute that counts as both a `Attribute msg` and a `Decoration`. +-} +type alias Attr decorative msg = + Internal.Attribute decorative msg + + +{-| Only decorations +-} +type alias Decoration = + Internal.Attribute Never Never + + +{-| -} +html : Html msg -> Element msg +html = + Internal.unstyled + + +{-| -} +htmlAttribute : Html.Attribute msg -> Attribute msg +htmlAttribute = + Internal.Attr + + +{-| -} +map : (msg -> msg1) -> Element msg -> Element msg1 +map = + Internal.map + + +{-| -} +mapAttribute : (msg -> msg1) -> Attribute msg -> Attribute msg1 +mapAttribute = + Internal.mapAttr + + +{-| -} +type alias Length = + Internal.Length + + +{-| -} +px : Int -> Length +px = + Internal.Px + + +{-| Shrink an element to fit it's contents. +-} +shrink : Length +shrink = + Internal.Content + + +{-| Fill the available space. The available space will be split evenly between elements that have `width fill`. +-} +fill : Length +fill = + Internal.Fill 1 + + +{-| Similarly you can set a minimum boundary. + + el + [ height + (fill + |> maximum 300 + |> minimum 30 + ) + + ] + (text "I will stop at 300px") + +-} +minimum : Int -> Length -> Length +minimum i l = + Internal.Min i l + + +{-| Add a maximum to a length. + + el + [ height + (fill + |> maximum 300 + ) + ] + (text "I will stop at 300px") + +-} +maximum : Int -> Length -> Length +maximum i l = + Internal.Max i l + + +{-| Sometimes you may not want to split available space evenly. In this case you can use `fillPortion` to define which elements should have what portion of the available space. + +So, two elements, one with `width (fillPortion 2)` and one with `width (fillPortion 3)`. The first would get 2 portions of the available space, while the second would get 3. + +**Also:** `fill == fillPortion 1` + +-} +fillPortion : Int -> Length +fillPortion = + Internal.Fill + + +{-| This is your top level node where you can turn `Element` into `Html`. +-} +layout : List (Attribute msg) -> Element msg -> Html msg +layout = + layoutWith { options = [] } + + +{-| -} +layoutWith : { options : List Option } -> List (Attribute msg) -> Element msg -> Html msg +layoutWith { options } attrs child = + Internal.renderRoot options + (Internal.htmlClass + (String.join " " + [ classes.root + , classes.any + , classes.single + + -- , classes.contentCenterX + -- , classes.contentCenterY + ] + ) + :: (Internal.rootStyle ++ attrs) + ) + child + + +{-| -} +type alias Option = + Internal.Option + + +{-| Style elements embeds two StyleSheets, one that is constant, and one that changes dynamically based on styles collected from the elments being rendered. + +This option will stop the static/constant stylesheet from rendering. + +Make sure to render the constant/static stylesheet at least once on your page! + +-} +noStaticStyleSheet : Option +noStaticStyleSheet = + Internal.RenderModeOption Internal.NoStaticStyleSheet + + +{-| -} +defaultFocus : + { borderColor : Maybe Color + , backgroundColor : Maybe Color + , shadow : + Maybe + { color : Color + , offset : ( Int, Int ) + , blur : Int + , size : Int + } + } +defaultFocus = + Internal.focusDefaultStyle + + +{-| -} +type alias FocusStyle = + { borderColor : Maybe Color + , backgroundColor : Maybe Color + , shadow : + Maybe + { color : Color + , offset : ( Int, Int ) + , blur : Int + , size : Int + } + } + + +{-| -} +focusStyle : FocusStyle -> Option +focusStyle = + Internal.FocusStyleOption + + +{-| Disable all `mouseOver` styles. +-} +noHover : Option +noHover = + Internal.HoverOption Internal.NoHover + + +{-| Any `hover` styles, aka attributes with `mouseOver` in the name, will be always turned on. + +This is useful for when you're targeting a platform that has no mouse, such as mobile. + +-} +forceHover : Option +forceHover = + Internal.HoverOption Internal.ForceHover + + +{-| Nothing to see here! +-} +none : Element msg +none = + Internal.Empty + + +{-| Create some plain text. + + text "Hello, you stylish developer!" + +**Note** text does not wrap by default. In order to get text to wrap, check out `paragraph`! + +-} +text : String -> Element msg +text content = + Internal.Text content + + +{-| The basic building block of your layout. + +You can think of an `el` as a `div`, but it can only hae one child. + +If you want multiple children, you'll need to use something like `row` or `column` + + import Color exposing (blue, darkBlue) + import Element exposing (Element) + import Element.Background as Background + import Element.Border as Border + + myElement : Element msg + myElement = + Element.el + [ Background.color blue + , Border.color darkBlue + ] + (Element.text "You've made a stylish element!") + +-} +el : List (Attribute msg) -> Element msg -> Element msg +el attrs child = + Internal.element + Internal.asEl + Internal.div + (width shrink + :: height shrink + :: attrs + ) + (Internal.Unkeyed [ child ]) + + +{-| If you want a row of elements, use `row`! +-} +row : List (Attribute msg) -> List (Element msg) -> Element msg +row attrs children = + Internal.element + Internal.asRow + Internal.div + (Internal.htmlClass (classes.contentLeft ++ " " ++ classes.contentCenterY) + :: width shrink + :: height shrink + :: attrs + ) + (Internal.Unkeyed children) + + +{-| -} +column : List (Attribute msg) -> List (Element msg) -> Element msg +column attrs children = + Internal.element + Internal.asColumn + Internal.div + (Internal.htmlClass + (classes.contentTop + ++ " " + ++ classes.contentLeft + ) + :: height shrink + :: width shrink + :: attrs + ) + (Internal.Unkeyed children) + + +{-| -} +wrappedRow : List (Attribute msg) -> List (Element msg) -> Element msg +wrappedRow attrs children = + let + ( padded, spaced ) = + Internal.extractSpacingAndPadding attrs + in + case spaced of + Nothing -> + Internal.element + Internal.asRow + Internal.div + (Internal.htmlClass + (classes.contentLeft + ++ " " + ++ classes.contentCenterY + ++ " " + ++ classes.wrapped + ) + :: width shrink + :: height shrink + :: attrs + ) + (Internal.Unkeyed children) + + Just (Internal.Spaced spaceName x y) -> + let + newPadding = + case padded of + Just (Internal.Padding name t r b l) -> + if r >= x && b >= y then + Just <| + Internal.StyleClass + Flag.padding + (Internal.PaddingStyle + (Internal.paddingName t + (r - x) + (b - y) + l + ) + t + (r - x) + (b - y) + l + ) + + else + Nothing + + Nothing -> + Nothing + in + case newPadding of + Just pad -> + Internal.element + Internal.asRow + Internal.div + (Internal.htmlClass + (classes.contentLeft + ++ " " + ++ classes.contentCenterY + ++ " " + ++ classes.wrapped + ) + :: width shrink + :: height shrink + :: attrs + ++ [ pad ] + ) + (Internal.Unkeyed children) + + Nothing -> + -- Not enough space in padding to compensate for spacing + Internal.element + Internal.asEl + Internal.div + attrs + (Internal.Unkeyed + [ Internal.element + Internal.asRow + Internal.div + (Internal.htmlClass + (classes.contentLeft + ++ " " + ++ classes.contentCenterY + ++ " " + ++ classes.wrapped + ) + :: Internal.Attr + (Html.Attributes.style "margin-bottom" (String.fromInt (negate y) ++ "px")) + :: width fill + :: height fill + :: Internal.StyleClass Flag.spacing (Internal.SpacingStyle spaceName x y) + :: [] + ) + (Internal.Unkeyed children) + ] + ) + + +{-| This is just an alias for `Debug.todo` +-} +type alias Todo = + String -> Never + + +{-| Highlight the borders of an element and it's children below. This can really help if you're running into some issue with your layout! + +**Note** This attribute needs to be handed `Debug.todo` in order to work, even though it won't do anything with it. This is a safety measure so you don't accidently ship code with `explain` in it, as Elm won't compile with `--optimize` if you still have a `Debug` statement in your code. + + el + [ Element.explain Debug.todo + ] + (text "Help! I'm being debugged!") + +-} +explain : Todo -> Attribute msg +explain _ = + Internal.htmlClass "explain" + + +{-| -} +type alias Column record msg = + { header : Element msg + , width : Length + , view : record -> Element msg + } + + +{-| Show some tabular data. + +Start with a list of records and specify how each column should be rendered. + +So, if we have a list of `persons`: + + type alias Person = + { firstName : String + , lastName : String + } + + persons : List Person + persons = + [ { firstName = "David" + , lastName = "Bowie" + } + , { firstName = "Florence" + , lastName = "Welch" + } + ] + +We could render it using + + Element.table [] + { data = persons + , columns = + [ { header = Element.text "First Name" + , width = fill + , view = + \person -> + Element.text person.firstName + } + , { header = Element.text "Last Name" + , width = fill + , view = + \person -> + Element.text person.lastName + } + ] + } + +**Note:** Sometimes you might not have a list of records directly in your model. In this case it can be really nice to write a function that transforms some part of your model into a list of records before feeding it into `Element.table`. + +-} +table : + List (Attribute msg) + -> + { data : List records + , columns : List (Column records msg) + } + -> Element msg +table attrs config = + tableHelper attrs + { data = config.data + , columns = + List.map InternalColumn config.columns + } + + +{-| -} +type alias IndexedColumn record msg = + { header : Element msg + , width : Length + , view : Int -> record -> Element msg + } + + +{-| Same as `Element.table` except the `view` for each column will also receive the row index as well as the record. +-} +indexedTable : + List (Attribute msg) + -> + { data : List records + , columns : List (IndexedColumn records msg) + } + -> Element msg +indexedTable attrs config = + tableHelper attrs + { data = config.data + , columns = + List.map InternalIndexedColumn config.columns + } + + +{-| -} +type alias InternalTable records msg = + { data : List records + , columns : List (InternalTableColumn records msg) + } + + +{-| -} +type InternalTableColumn record msg + = InternalIndexedColumn (IndexedColumn record msg) + | InternalColumn (Column record msg) + + +tableHelper : List (Attribute msg) -> InternalTable data msg -> Element msg +tableHelper attrs config = + let + ( sX, sY ) = + Internal.getSpacing attrs ( 0, 0 ) + + columnHeader col = + case col of + InternalIndexedColumn colConfig -> + colConfig.header + + InternalColumn colConfig -> + colConfig.header + + columnWidth col = + case col of + InternalIndexedColumn colConfig -> + colConfig.width + + InternalColumn colConfig -> + colConfig.width + + maybeHeaders = + List.map columnHeader config.columns + |> (\headers -> + if List.all ((==) Internal.Empty) headers then + Nothing + + else + Just (List.indexedMap (\col header -> onGrid 1 (col + 1) header) headers) + ) + + template = + Internal.StyleClass Flag.gridTemplate <| + Internal.GridTemplateStyle + { spacing = ( px sX, px sY ) + , columns = List.map columnWidth config.columns + , rows = List.repeat (List.length config.data) Internal.Content + } + + onGrid rowLevel columnLevel elem = + Internal.element + Internal.asEl + Internal.div + [ Internal.StyleClass Flag.gridPosition + (Internal.GridPosition + { row = rowLevel + , col = columnLevel + , width = 1 + , height = 1 + } + ) + ] + (Internal.Unkeyed [ elem ]) + + add cell columnConfig cursor = + case columnConfig of + InternalIndexedColumn col -> + { cursor + | elements = + onGrid cursor.row + cursor.column + (col.view + (if maybeHeaders == Nothing then + cursor.row - 1 + + else + cursor.row - 2 + ) + cell + ) + :: cursor.elements + , column = cursor.column + 1 + } + + InternalColumn col -> + { cursor + | elements = + onGrid cursor.row cursor.column (col.view cell) + :: cursor.elements + , column = cursor.column + 1 + } + + build columns rowData cursor = + let + newCursor = + List.foldl (add rowData) + cursor + columns + in + { newCursor + | row = cursor.row + 1 + , column = 1 + } + + children = + List.foldl (build config.columns) + { elements = [] + , row = + if maybeHeaders == Nothing then + 1 + + else + 2 + , column = 1 + } + config.data + in + Internal.element + Internal.asGrid + Internal.div + (width fill + :: template + :: attrs + ) + (Internal.Unkeyed + (case maybeHeaders of + Nothing -> + children.elements + + Just renderedHeaders -> + renderedHeaders ++ children.elements + ) + ) + + +{-| A paragraph will layout all children as wrapped, inline elements. + + import Element + import Element.Font as Font + + Element.paragraph [] + [ text "lots of text ...." + , el [ Font.bold ] (text "this is bold") + , text "lots of text ...." + ] + +This is really useful when you want to markup text by having some parts be bold, or some be links, or whatever you so desire. + +Also, if a child element has `alignLeft` or `alignRight`, then it will be moved to that side and the text will flow around it, (ah yes, `float` behavior). + +This makes it particularly easy to do something like a [dropped capital](https://en.wikipedia.org/wiki/Initial). + + import Element + import Element.Font as Font + + Element.paragraph [] + [ el + [ alignLeft + , padding 5 + , Font.lineHeight 1 + ] + (text "S") + , text "o much text ...." + ] + +Which will look something like + +![A paragraph where the first letter is twice the height of the others](https://mdgriffith.gitbooks.io/style-elements/content/assets/Screen%20Shot%202017-08-25%20at%209.41.52%20PM.png) + +-} +paragraph : List (Attribute msg) -> List (Element msg) -> Element msg +paragraph attrs children = + Internal.element + Internal.asParagraph + Internal.div + (Internal.Describe Internal.Paragraph + :: width fill + :: spacing 5 + :: attrs + ) + (Internal.Unkeyed children) + + +{-| Now that we have a paragraph, we need some way to attach a bunch of paragraph's together. + +To do that we can use a `textColumn`. + +The main difference between a `column` and a `textColumn` is that `textColumn` will flow the text around elements that have `alignRight` or `alignLeft`, just like we just saw with paragraph. + +In the following example, we have a `textColumn` where one child has `alignLeft`. + + Element.textColumn [ spacing 10, padding 10 ] + [ paragraph [] [ text "lots of text ...." ] + , el [ alignLeft ] none + , paragraph [] [ text "lots of text ...." ] + ] + +Which will result in something like: + +![A text layout where an image is on the left.](https://mdgriffith.gitbooks.io/style-elements/content/assets/Screen%20Shot%202017-08-25%20at%208.42.39%20PM.png) + +-} +textColumn : List (Attribute msg) -> List (Element msg) -> Element msg +textColumn attrs children = + Internal.element + Internal.asTextColumn + Internal.div + (width + (fill + |> minimum 500 + |> maximum 750 + ) + :: attrs + ) + (Internal.Unkeyed children) + + +{-| Both a source and a description are required for images. + +The description is used for people using screen readers. + +Leaving the description blank will cause the image to be ignored by assistive technology. This can make sense for images that are purely decorative and add no additional information. + +So, take a moment to describe your image as you would to someone who has a harder time seeing. + +-} +image : List (Attribute msg) -> { src : String, description : String } -> Element msg +image attrs { src, description } = + let + imageAttributes = + attrs + |> List.filter + (\a -> + case a of + Internal.Width _ -> + True + + Internal.Height _ -> + True + + _ -> + False + ) + in + Internal.element + Internal.asEl + Internal.div + (Internal.htmlClass classes.imageContainer + :: attrs + ) + (Internal.Unkeyed + [ Internal.element + Internal.asEl + (Internal.NodeName "img") + ([ Internal.Attr <| Html.Attributes.src src + , Internal.Attr <| Html.Attributes.alt description + ] + ++ imageAttributes + ) + (Internal.Unkeyed []) + ] + ) + + +{-| + + link [] + { url = "http://fruits.com" + , label = text "A link to my favorite fruit provider." + } + +-} +link : + List (Attribute msg) + -> + { url : String + , label : Element msg + } + -> Element msg +link attrs { url, label } = + Internal.element + Internal.asEl + (Internal.NodeName "a") + (Internal.Attr (Html.Attributes.href url) + :: Internal.Attr (Html.Attributes.rel "noopener noreferrer") + :: width shrink + :: height shrink + :: Internal.htmlClass + (classes.contentCenterX + ++ " " + ++ classes.contentCenterY + ) + :: attrs + ) + (Internal.Unkeyed [ label ]) + + +{-| -} +newTabLink : + List (Attribute msg) + -> + { url : String + , label : Element msg + } + -> Element msg +newTabLink attrs { url, label } = + Internal.element + Internal.asEl + (Internal.NodeName "a") + (Internal.Attr (Html.Attributes.href url) + :: Internal.Attr (Html.Attributes.rel "noopener noreferrer") + :: Internal.Attr (Html.Attributes.target "_blank") + :: width shrink + :: height shrink + :: Internal.htmlClass (classes.contentCenterX ++ " " ++ classes.contentCenterY) + :: attrs + ) + (Internal.Unkeyed [ label ]) + + +{-| A link to download a file. +-} +download : + List (Attribute msg) + -> + { url : String + , label : Element msg + } + -> Element msg +download attrs { url, label } = + Internal.element + Internal.asEl + (Internal.NodeName "a") + (Internal.Attr (Html.Attributes.href url) + :: Internal.Attr (Html.Attributes.download "") + :: width shrink + :: height shrink + :: Internal.htmlClass classes.contentCenterX + :: Internal.htmlClass classes.contentCenterY + :: attrs + ) + (Internal.Unkeyed [ label ]) + + +{-| A link to download a file, but you can specify the filename. +-} +downloadAs : + List (Attribute msg) + -> + { label : Element msg + , filename : String + , url : String + } + -> Element msg +downloadAs attrs { url, filename, label } = + Internal.element + Internal.asEl + (Internal.NodeName "a") + (Internal.Attr (Html.Attributes.href url) + :: Internal.Attr (Html.Attributes.download filename) + :: width shrink + :: height shrink + :: Internal.htmlClass classes.contentCenterX + :: Internal.htmlClass classes.contentCenterY + :: attrs + ) + (Internal.Unkeyed [ label ]) + + +{-| -} +below : Element msg -> Attribute msg +below element = + Internal.Nearby Internal.Below element + + +{-| -} +above : Element msg -> Attribute msg +above element = + Internal.Nearby Internal.Above element + + +{-| -} +onRight : Element msg -> Attribute msg +onRight element = + Internal.Nearby Internal.OnRight element + + +{-| -} +onLeft : Element msg -> Attribute msg +onLeft element = + Internal.Nearby Internal.OnLeft element + + +{-| This will place an element in front of another. + +**Note:** If you use this on a `layout` element, it will place the element as fixed to the viewport which can be useful for modals and overlays. + +-} +inFront : Element msg -> Attribute msg +inFront element = + Internal.Nearby Internal.InFront element + + +{-| This will place an element between the background and the content of an element. +-} +behindContent : Element msg -> Attribute msg +behindContent element = + Internal.Nearby Internal.Behind element + + +{-| -} +width : Length -> Attribute msg +width = + Internal.Width + + +{-| -} +height : Length -> Attribute msg +height = + Internal.Height + + +{-| -} +scale : Float -> Attr decorative msg +scale n = + Internal.TransformComponent Flag.scale (Internal.Scale ( n, n, 1 )) + + +{-| -} +rotate : Float -> Attr decorative msg +rotate angle = + Internal.TransformComponent Flag.rotate (Internal.Rotate ( 0, 0, 1 ) angle) + + +{-| -} +moveUp : Float -> Attr decorative msg +moveUp y = + Internal.TransformComponent Flag.moveY (Internal.MoveY (negate y)) + + +{-| -} +moveDown : Float -> Attr decorative msg +moveDown y = + Internal.TransformComponent Flag.moveY (Internal.MoveY y) + + +{-| -} +moveRight : Float -> Attr decorative msg +moveRight x = + Internal.TransformComponent Flag.moveX (Internal.MoveX x) + + +{-| -} +moveLeft : Float -> Attr decorative msg +moveLeft x = + Internal.TransformComponent Flag.moveX (Internal.MoveX (negate x)) + + +{-| -} +padding : Int -> Attribute msg +padding x = + Internal.StyleClass Flag.padding (Internal.PaddingStyle ("p-" ++ String.fromInt x) x x x x) + + +{-| Set horizontal and vertical padding. +-} +paddingXY : Int -> Int -> Attribute msg +paddingXY x y = + if x == y then + Internal.StyleClass Flag.padding (Internal.PaddingStyle ("p-" ++ String.fromInt x) x x x x) + + else + Internal.StyleClass Flag.padding + (Internal.PaddingStyle + ("p-" ++ String.fromInt x ++ "-" ++ String.fromInt y) + y + x + y + x + ) + + +{-| If you find yourself defining unique paddings all the time, you might consider defining + + edges = + { top = 0 + , right = 0 + , bottom = 0 + , left = 0 + } + +And then just do + + paddingEach { edges | right 5 } + +-} +paddingEach : { top : Int, right : Int, bottom : Int, left : Int } -> Attribute msg +paddingEach { top, right, bottom, left } = + if top == right && top == bottom && top == left then + Internal.StyleClass Flag.padding (Internal.PaddingStyle ("p-" ++ String.fromInt top) top top top top) + + else + Internal.StyleClass Flag.padding + (Internal.PaddingStyle + (Internal.paddingName top right bottom left) + top + right + bottom + left + ) + + +{-| -} +centerX : Attribute msg +centerX = + Internal.AlignX Internal.CenterX + + +{-| -} +centerY : Attribute msg +centerY = + Internal.AlignY Internal.CenterY + + +{-| -} +alignTop : Attribute msg +alignTop = + Internal.AlignY Internal.Top + + +{-| -} +alignBottom : Attribute msg +alignBottom = + Internal.AlignY Internal.Bottom + + +{-| -} +alignLeft : Attribute msg +alignLeft = + Internal.AlignX Internal.Left + + +{-| -} +alignRight : Attribute msg +alignRight = + Internal.AlignX Internal.Right + + +{-| -} +spaceEvenly : Attribute msg +spaceEvenly = + Internal.Class Flag.xAlign (.spaceEvenly Internal.Style.classes) + + +{-| -} +spacing : Int -> Attribute msg +spacing x = + Internal.StyleClass Flag.spacing (Internal.SpacingStyle (Internal.spacingName x x) x x) + + +{-| In the majority of cases you'll just need to use `spacing`, which will work as intended. + +However for some layouts, like `textColumn`, you may want to set a different spacing for the x axis compared to the y axis. + +-} +spacingXY : Int -> Int -> Attribute msg +spacingXY x y = + Internal.StyleClass Flag.spacing (Internal.SpacingStyle (Internal.spacingName x y) x y) + + +{-| Make an element transparent and have it ignore any mouse or touch events, though it will stil take up space. +-} +transparent : Bool -> Attr decorative msg +transparent on = + if on then + Internal.StyleClass Flag.transparency (Internal.Transparency "transparent" 1.0) + + else + Internal.StyleClass Flag.transparency (Internal.Transparency "visible" 0.0) + + +{-| A capped value between 0.0 and 1.0, where 0.0 is transparent and 1.0 is fully opaque. + +Semantically equavalent to html opacity. + +-} +alpha : Float -> Attr decorative msg +alpha o = + let + transparency = + o + |> max 0.0 + |> min 1.0 + |> (\x -> 1 - x) + in + Internal.StyleClass Flag.transparency <| Internal.Transparency ("transparency-" ++ Internal.floatClass transparency) transparency + + + +-- {-| -} +-- hidden : Bool -> Attribute msg +-- hidden on = +-- if on then +-- Internal.class "hidden" +-- else +-- Internal.NoAttribute + + +{-| -} +scrollbars : Attribute msg +scrollbars = + Internal.Class Flag.overflow classes.scrollbars + + +{-| -} +scrollbarY : Attribute msg +scrollbarY = + Internal.Class Flag.overflow classes.scrollbarsY + + +{-| -} +scrollbarX : Attribute msg +scrollbarX = + Internal.Class Flag.overflow classes.scrollbarsX + + +{-| -} +clip : Attribute msg +clip = + Internal.Class Flag.overflow classes.clip + + +{-| -} +clipY : Attribute msg +clipY = + Internal.Class Flag.overflow classes.clipY + + +{-| -} +clipX : Attribute msg +clipX = + Internal.Class Flag.overflow classes.clipX + + +{-| Set the cursor to be a pointing hand when it's hovering over this element. +-} +pointer : Attribute msg +pointer = + Internal.Class Flag.cursor classes.cursorPointer + + +{-| -} +type alias Device = + { class : DeviceClass + , orientation : Orientation + } + + +{-| -} +type DeviceClass + = Phone + | Tablet + | Desktop + | BigDesktop + + +{-| -} +type Orientation + = Portrait + | Landscape + + +{-| Takes in a Window.Size and returns a device profile which can be used for responsiveness. +-} +classifyDevice : { window | height : Int, width : Int } -> Device +classifyDevice window = + { class = + if window.width <= 600 then + Phone + + else if window.width > 600 && window.width <= 1200 then + Tablet + + else if window.width > 1200 && window.width <= 1800 then + Desktop + + else + BigDesktop + , orientation = + if window.width < window.height then + Portrait + + else + Landscape + } + + +{-| When designing it's nice to use a modular scale to set spacial rythms. + + scaled = + Scale.modular 16 1.25 + +A modular scale starts with a number, and multiplies it by a ratio a number of times. +Then, when setting font sizes you can use: + + Font.size (scaled 1) -- results in 16 + + Font.size (scaled 2) -- 16 * 1.25 results in 20 + + Font.size (scaled 4) -- 16 * 1.25 ^ (4 - 1) results in 31.25 + +We can also provide negative numbers to scale below 16px. + + Font.size (scaled -1) -- 16 * 1.25 ^ (-1) results in 12.8 + +-} +modular : Float -> Float -> Int -> Float +modular normal ratio rescale = + if rescale == 0 then + normal + + else if rescale < 0 then + normal * ratio ^ toFloat rescale + + else + normal * ratio ^ (toFloat rescale - 1) + + +{-| -} +mouseOver : List Decoration -> Attribute msg +mouseOver decs = + Internal.StyleClass Flag.hover <| + Internal.PseudoSelector Internal.Hover + (Internal.unwrapDecorations decs) + + +{-| -} +mouseDown : List Decoration -> Attribute msg +mouseDown decs = + Internal.StyleClass Flag.active <| + Internal.PseudoSelector Internal.Active + (Internal.unwrapDecorations decs) + + +{-| -} +focused : List Decoration -> Attribute msg +focused decs = + Internal.StyleClass Flag.focus <| + Internal.PseudoSelector Internal.Focus + (Internal.unwrapDecorations decs) diff --git a/src/Element/Background.elm b/src/Element/Background.elm new file mode 100644 index 0000000..acf39d0 --- /dev/null +++ b/src/Element/Background.elm @@ -0,0 +1,232 @@ +module Element.Background + exposing + ( color + , gradient + , image + , tiled + , tiledX + , tiledY + , uncropped + ) + +{-| + +@docs color, gradient + + +# Images + +@docs image, uncropped, tiled, tiledX, tiledY + +**Note** if you want more control over a background image than is provided here, you should try just using a normal `Element.image` with something like `Element.behind`. + +-} + +import Element exposing (Attr, Attribute, Color) +import Internal.Flag as Flag +import Internal.Model as Internal +import VirtualDom + + +{-| -} +color : Color -> Attr decorative msg +color clr = + Internal.StyleClass Flag.bgColor (Internal.Colored ("bg-" ++ Internal.formatColorClass clr) "background-color" clr) + + +{-| Resize the image to fit the containing element while maintaining proportions and cropping the overflow. +-} +image : String -> Attribute msg +image src = + Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") center / cover no-repeat")) + + +{-| A centered background image that keeps it's natural propostions, but scales to fit the space. +-} +uncropped : String -> Attribute msg +uncropped src = + Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") center / contain no-repeat")) + + +{-| Tile an image in the x and y axes. +-} +tiled : String -> Attribute msg +tiled src = + Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") repeat")) + + +{-| Tile an image in the x axis. +-} +tiledX : String -> Attribute msg +tiledX src = + Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") repeat-x")) + + +{-| Tile an image in the y axis. +-} +tiledY : String -> Attribute msg +tiledY src = + Internal.Attr (VirtualDom.style "background" ("url(\"" ++ src ++ "\") repeat-y")) + + +type Direction + = ToUp + | ToDown + | ToRight + | ToTopRight + | ToBottomRight + | ToLeft + | ToTopLeft + | ToBottomLeft + | ToAngle Float + + +type Step + = ColorStep Color + | PercentStep Float Color + | PxStep Int Color + + +{-| -} +step : Color -> Step +step = + ColorStep + + +{-| -} +percent : Float -> Color -> Step +percent = + PercentStep + + +{-| -} +px : Int -> Color -> Step +px = + PxStep + + +{-| A linear gradient. + +First you need to specify what direction the gradient is going by providing an angle in radians. `0` is up and `pi` is down. + +The colors will be evenly spaced. + +-} +gradient : + { angle : Float + , steps : List Color + } + -> Attr decorative msg +gradient { angle, steps } = + case steps of + [] -> + Internal.NoAttribute + + clr :: [] -> + Internal.StyleClass Flag.bgColor + (Internal.Colored ("bg-" ++ Internal.formatColorClass clr) "background-color" clr) + + _ -> + Internal.StyleClass Flag.bgGradient <| + Internal.Single ("bg-grad-" ++ (String.join "-" <| Internal.floatClass angle :: List.map Internal.formatColorClass steps)) + "background-image" + ("linear-gradient(" ++ (String.join ", " <| (String.fromFloat angle ++ "rad") :: List.map Internal.formatColor steps) ++ ")") + + + +-- {-| -} +-- gradientWith : { direction : Direction, steps : List Step } -> Attribute msg +-- gradientWith { direction, steps } = +-- StyleClass <| +-- Single ("bg-gradient-" ++ (String.join "-" <| renderDirectionClass direction :: List.map renderStepClass steps)) +-- "background" +-- ("linear-gradient(" ++ (String.join ", " <| renderDirection direction :: List.map renderStep steps) ++ ")") +-- {-| -} +-- renderStep : Step -> String +-- renderStep step = +-- case step of +-- ColorStep color -> +-- formatColor color +-- PercentStep percent color -> +-- formatColor color ++ " " ++ toString percent ++ "%" +-- PxStep px color -> +-- formatColor color ++ " " ++ toString px ++ "px" +-- {-| -} +-- renderStepClass : Step -> String +-- renderStepClass step = +-- case step of +-- ColorStep color -> +-- formatColorClass color +-- PercentStep percent color -> +-- formatColorClass color ++ "-" ++ floatClass percent ++ "p" +-- PxStep px color -> +-- formatColorClass color ++ "-" ++ toString px ++ "px" +-- toUp : Direction +-- toUp = +-- ToUp +-- toDown : Direction +-- toDown = +-- ToDown +-- toRight : Direction +-- toRight = +-- ToRight +-- toTopRight : Direction +-- toTopRight = +-- ToTopRight +-- toBottomRight : Direction +-- toBottomRight = +-- ToBottomRight +-- toLeft : Direction +-- toLeft = +-- ToLeft +-- toTopLeft : Direction +-- toTopLeft = +-- ToTopLeft +-- toBottomLeft : Direction +-- toBottomLeft = +-- ToBottomLeft +-- angle : Float -> Direction +-- angle rad = +-- ToAngle rad +-- renderDirection : Direction -> String +-- renderDirection dir = +-- case dir of +-- ToUp -> +-- "to top" +-- ToDown -> +-- "to bottom" +-- ToRight -> +-- "to right" +-- ToTopRight -> +-- "to top right" +-- ToBottomRight -> +-- "to bottom right" +-- ToLeft -> +-- "to left" +-- ToTopLeft -> +-- "to top left" +-- ToBottomLeft -> +-- "to bottom left" +-- ToAngle angle -> +-- toString angle ++ "rad" +-- renderDirectionClass : Direction -> String +-- renderDirectionClass dir = +-- case dir of +-- ToUp -> +-- "to-top" +-- ToDown -> +-- "to-bottom" +-- ToRight -> +-- "to-right" +-- ToTopRight -> +-- "to-top-right" +-- ToBottomRight -> +-- "to-bottom-right" +-- ToLeft -> +-- "to-left" +-- ToTopLeft -> +-- "to-top-left" +-- ToBottomLeft -> +-- "to-bottom-left" +-- ToAngle angle -> +-- floatClass angle ++ "rad" diff --git a/src/Element/Border.elm b/src/Element/Border.elm new file mode 100644 index 0000000..30ece69 --- /dev/null +++ b/src/Element/Border.elm @@ -0,0 +1,221 @@ +module Element.Border + exposing + ( color + , dashed + , dotted + , glow + , innerGlow + , innerShadow + , roundEach + , rounded + , shadow + , solid + , width + , widthEach + , widthXY + ) + +{-| + +@docs color + + +## Border Widths + +@docs width, widthXY, widthEach + + +## Border Styles + +@docs solid, dashed, dotted + + +## Rounded Corners + +@docs rounded, roundEach + + +## Shadows + +@docs glow, innerGlow, shadow, innerShadow + +-} + +import Element exposing (Attr, Attribute, Color) +import Internal.Flag as Flag +import Internal.Model as Internal +import Internal.Style as Style exposing (classes) + + +{-| -} +color : Color -> Attr decorative msg +color clr = + Internal.StyleClass Flag.borderColor (Internal.Colored ("border-color-" ++ Internal.formatColorClass clr) "border-color" clr) + + +{-| -} +width : Int -> Attribute msg +width v = + Internal.StyleClass Flag.borderWidth (Internal.Single ("border-" ++ String.fromInt v) "border-width" (String.fromInt v ++ "px")) + + +{-| Set horizontal and vertical borders. +-} +widthXY : Int -> Int -> Attribute msg +widthXY x y = + Internal.StyleClass Flag.borderWidth (Internal.Single ("border-" ++ String.fromInt x ++ "-" ++ String.fromInt y) "border-width" (String.fromInt y ++ "px " ++ String.fromInt x ++ "px")) + + +{-| -} +widthEach : { bottom : Int, left : Int, right : Int, top : Int } -> Attribute msg +widthEach { bottom, top, left, right } = + Internal.StyleClass Flag.borderWidth + (Internal.Single ("border-" ++ String.fromInt top ++ "-" ++ String.fromInt right ++ String.fromInt bottom ++ "-" ++ String.fromInt left) + "border-width" + (String.fromInt top + ++ "px " + ++ String.fromInt right + ++ "px " + ++ String.fromInt bottom + ++ "px " + ++ String.fromInt left + ++ "px" + ) + ) + + + +-- {-| No Borders +-- -} +-- none : Attribute msg +-- none = +-- Class "border" "border-none" + + +{-| -} +solid : Attribute msg +solid = + Internal.Class Flag.borderStyle classes.borderSolid + + +{-| -} +dashed : Attribute msg +dashed = + Internal.Class Flag.borderStyle classes.borderDashed + + +{-| -} +dotted : Attribute msg +dotted = + Internal.Class Flag.borderStyle classes.borderDotted + + +{-| Round all corners. +-} +rounded : Int -> Attribute msg +rounded radius = + Internal.StyleClass Flag.borderRound (Internal.Single ("border-radius-" ++ String.fromInt radius) "border-radius" (String.fromInt radius ++ "px")) + + +{-| -} +roundEach : { topLeft : Int, topRight : Int, bottomLeft : Int, bottomRight : Int } -> Attribute msg +roundEach { topLeft, topRight, bottomLeft, bottomRight } = + Internal.StyleClass Flag.borderRound + (Internal.Single ("border-radius-" ++ String.fromInt topLeft ++ "-" ++ String.fromInt topRight ++ String.fromInt bottomLeft ++ "-" ++ String.fromInt bottomRight) + "border-radius" + (String.fromInt topLeft + ++ "px " + ++ String.fromInt topRight + ++ "px " + ++ String.fromInt bottomRight + ++ "px " + ++ String.fromInt bottomLeft + ++ "px" + ) + ) + + +{-| A simple glow by specifying the color and size. +-} +glow : Color -> Float -> Attr decorative msg +glow clr size = + shadow + { offset = ( 0, 0 ) + , size = size + , blur = size * 2 + , color = clr + } + + +{-| -} +innerGlow : Color -> Float -> Attr decorative msg +innerGlow clr size = + innerShadow + { offset = ( 0, 0 ) + , size = size + , blur = size * 2 + , color = clr + } + + +{-| -} +shadow : + { offset : ( Float, Float ) + , size : Float + , blur : Float + , color : Color + } + -> Attr decorative msg +shadow almostShade = + let + shade = + { inset = False + , offset = almostShade.offset + , size = almostShade.size + , blur = almostShade.blur + , color = almostShade.color + } + in + Internal.StyleClass Flag.shadows <| + Internal.Single (Internal.boxShadowName shade) "box-shadow" (Internal.formatBoxShadow shade) + + +{-| -} +innerShadow : + { offset : ( Float, Float ) + , size : Float + , blur : Float + , color : Color + } + -> Attr decorative msg +innerShadow almostShade = + let + shade = + { inset = True + , offset = almostShade.offset + , size = almostShade.size + , blur = almostShade.blur + , color = almostShade.color + } + in + Internal.StyleClass Flag.shadows <| + Internal.Single (Internal.boxShadowName shade) "box-shadow" (Internal.formatBoxShadow shade) + + + +-- {-| -} +-- shadow : +-- { offset : ( Float, Float ) +-- , blur : Float +-- , size : Float +-- , color : Color +-- } +-- -> Attr decorative msg +-- shadow shade = +-- Internal.BoxShadow +-- { inset = False +-- , offset = shade.offset +-- , size = shade.size +-- , blur = shade.blur +-- , color = shade.color +-- } diff --git a/src/Element/Events.elm b/src/Element/Events.elm new file mode 100644 index 0000000..d0afe68 --- /dev/null +++ b/src/Element/Events.elm @@ -0,0 +1,272 @@ +module Element.Events + exposing + ( onClick + -- , onClickCoords + -- , onClickPageCoords + -- , onClickScreenCoords + , onDoubleClick + , onFocus + , onLoseFocus + -- , onMouseCoords + , onMouseDown + , onMouseEnter + , onMouseLeave + , onMouseMove + -- , onMousePageCoords + -- , onMouseScreenCoords + , onMouseUp + ) + +{-| + + +## Mouse Events + +@docs onClick, onDoubleClick, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave, onMouseMove + + +## Focus Events + +@docs onFocus, onLoseFocus + +-} + +import Element exposing (Attribute) +import Html.Events +import Internal.Model as Internal +import Json.Decode as Json +import VirtualDom + + +-- MOUSE EVENTS + + +{-| -} +onMouseDown : msg -> Attribute msg +onMouseDown = + Internal.Attr << Html.Events.onMouseDown + + +{-| -} +onMouseUp : msg -> Attribute msg +onMouseUp = + Internal.Attr << Html.Events.onMouseUp + + +{-| -} +onClick : msg -> Attribute msg +onClick = + Internal.Attr << Html.Events.onClick + + +{-| -} +onDoubleClick : msg -> Attribute msg +onDoubleClick = + Internal.Attr << Html.Events.onDoubleClick + + +{-| -} +onMouseEnter : msg -> Attribute msg +onMouseEnter = + Internal.Attr << Html.Events.onMouseEnter + + +{-| -} +onMouseLeave : msg -> Attribute msg +onMouseLeave = + Internal.Attr << Html.Events.onMouseLeave + + +{-| -} +onMouseMove : msg -> Attribute msg +onMouseMove msg = + on "mousemove" (Json.succeed msg) + + + +-- onClickWith +-- { button = primary +-- , send = localCoords Button +-- } +-- type alias Click = +-- { button : Button +-- , send : Track +-- } +-- type Button = Primary | Secondary +-- type Track +-- = ElementCoords +-- | PageCoords +-- | ScreenCoords +-- | + + +{-| -} +onClickCoords : (Coords -> msg) -> Attribute msg +onClickCoords msg = + on "click" (Json.map msg localCoords) + + +{-| -} +onClickScreenCoords : (Coords -> msg) -> Attribute msg +onClickScreenCoords msg = + on "click" (Json.map msg screenCoords) + + +{-| -} +onClickPageCoords : (Coords -> msg) -> Attribute msg +onClickPageCoords msg = + on "click" (Json.map msg pageCoords) + + +{-| -} +onMouseCoords : (Coords -> msg) -> Attribute msg +onMouseCoords msg = + on "mousemove" (Json.map msg localCoords) + + +{-| -} +onMouseScreenCoords : (Coords -> msg) -> Attribute msg +onMouseScreenCoords msg = + on "mousemove" (Json.map msg screenCoords) + + +{-| -} +onMousePageCoords : (Coords -> msg) -> Attribute msg +onMousePageCoords msg = + on "mousemove" (Json.map msg pageCoords) + + +type alias Coords = + { x : Int + , y : Int + } + + +screenCoords : Json.Decoder Coords +screenCoords = + Json.map2 Coords + (Json.field "screenX" Json.int) + (Json.field "screenY" Json.int) + + +{-| -} +localCoords : Json.Decoder Coords +localCoords = + Json.map2 Coords + (Json.field "offsetX" Json.int) + (Json.field "offsetY" Json.int) + + +pageCoords : Json.Decoder Coords +pageCoords = + Json.map2 Coords + (Json.field "pageX" Json.int) + (Json.field "pageY" Json.int) + + + +-- FOCUS EVENTS + + +{-| -} +onLoseFocus : msg -> Attribute msg +onLoseFocus = + Internal.Attr << Html.Events.onBlur + + +{-| -} +onFocus : msg -> Attribute msg +onFocus = + Internal.Attr << Html.Events.onFocus + + + +-- CUSTOM EVENTS + + +{-| Create a custom event listener. Normally this will not be necessary, but +you have the power! Here is how `onClick` is defined for example: + + import Json.Decode as Json + + onClick : msg -> Attribute msg + onClick message = + on "click" (Json.succeed message) + +The first argument is the event name in the same format as with JavaScript's +[`addEventListener`][aEL] function. +The second argument is a JSON decoder. Read more about these [here][decoder]. +When an event occurs, the decoder tries to turn the event object into an Elm +value. If successful, the value is routed to your `update` function. In the +case of `onClick` we always just succeed with the given `message`. +If this is confusing, work through the [Elm Architecture Tutorial][tutorial]. +It really does help! +[aEL]: +[decoder]: +[tutorial]: + +-} +on : String -> Json.Decoder msg -> Attribute msg +on event decode = + Internal.Attr <| Html.Events.on event decode + + + +-- {-| Same as `on` but you can set a few options. +-- -} +-- onWithOptions : String -> Html.Events.Options -> Json.Decoder msg -> Attribute msg +-- onWithOptions event options decode = +-- Internal.Attr <| Html.Events.onWithOptions event options decode +-- COMMON DECODERS + + +{-| A `Json.Decoder` for grabbing `event.target.value`. We use this to define +`onInput` as follows: + + import Json.Decode as Json + + onInput : (String -> msg) -> Attribute msg + onInput tagger = + on "input" (Json.map tagger targetValue) + +You probably will never need this, but hopefully it gives some insights into +how to make custom event handlers. + +-} +targetValue : Json.Decoder String +targetValue = + Json.at [ "target", "value" ] Json.string + + +{-| A `Json.Decoder` for grabbing `event.target.checked`. We use this to define +`onCheck` as follows: + + import Json.Decode as Json + + onCheck : (Bool -> msg) -> Attribute msg + onCheck tagger = + on "input" (Json.map tagger targetChecked) + +-} +targetChecked : Json.Decoder Bool +targetChecked = + Json.at [ "target", "checked" ] Json.bool + + +{-| A `Json.Decoder` for grabbing `event.keyCode`. This helps you define +keyboard listeners like this: + + import Json.Decode as Json + + onKeyUp : (Int -> msg) -> Attribute msg + onKeyUp tagger = + on "keyup" (Json.map tagger keyCode) + +**Note:** It looks like the spec is moving away from `event.keyCode` and +towards `event.key`. Once this is supported in more browsers, we may add +helpers here for `onKeyUp`, `onKeyDown`, `onKeyPress`, etc. + +-} +keyCode : Json.Decoder Int +keyCode = + Json.field "keyCode" Json.int diff --git a/src/Element/Font.elm b/src/Element/Font.elm new file mode 100644 index 0000000..353415d --- /dev/null +++ b/src/Element/Font.elm @@ -0,0 +1,332 @@ +module Element.Font + exposing + ( Font + , alignLeft + , alignRight + , bold + , center + , color + , external + , extraBold + , extraLight + , family + , glow + , hairline + , heavy + , italic + , justify + , letterSpacing + , light + , medium + , monospace + , regular + , sansSerif + , semiBold + , serif + , shadow + , size + , strike + , typeface + , underline + , unitalicized + , wordSpacing + ) + +{-| + + import Color exposing (blue) + import Element + import Element.Font as Font + + view = + Element.el + [ Font.color blue + , Font.size 18 + , Font.family + [ Font.typeface "Open Sans" + , Font.sansSerif + ] + ] + (Element.text "Woohoo, I'm stylish text") + +**Note**: `Font.color`, `Font.size`, and `Font.family` are inherited, meaning you can set them at the top of your view and all subsequent nodes will have that value. + +@docs color, size + + +## Typefaces + +@docs family, Font, typeface, serif, sansSerif, monospace + +@docs external + +`Font.external` can be used to import font files. Let's say you found a neat font on : + + import Element + import Element.Font as Font + + view = + Element.el + [ Font.family + [ Font.external + { name = "Roboto" + , url = "https://fonts.googleapis.com/css?family=Roboto" + } + , Font.sansSerif + ] + ] + (Element.text "Woohoo, I'm stylish text") + + +## Alignment and Spacing + +@docs alignLeft, alignRight, center, justify, letterSpacing, wordSpacing + + +## Font Styles + +@docs underline, strike, italic, unitalicized + + +## Font Weight + +@docs heavy, extraBold, bold, semiBold, medium, regular, light, extraLight, hairline + + +## Shadows + +@docs glow, shadow + +-} + +import Element exposing (Attr, Attribute, Color) +import Internal.Flag as Flag +import Internal.Model as Internal +import Internal.Style exposing (classes) + + +{-| -} +type alias Font = + Internal.Font + + +{-| -} +color : Color -> Attr decorative msg +color fontColor = + Internal.StyleClass Flag.fontColor (Internal.Colored ("fc-" ++ Internal.formatColorClass fontColor) "color" fontColor) + + +{-| + + import Element + import Element.Font as Font + + myElement = + Element.el + [ Font.family + [ Font.typeface "Helvetica" + , Font.sansSerif + ] + ] + (text "") + +-} +family : List Font -> Attribute msg +family families = + Internal.StyleClass Flag.fontFamily <| Internal.FontFamily (List.foldl Internal.renderFontClassName "ff-" families) families + + +{-| -} +serif : Font +serif = + Internal.Serif + + +{-| -} +sansSerif : Font +sansSerif = + Internal.SansSerif + + +{-| -} +monospace : Font +monospace = + Internal.Monospace + + +{-| -} +typeface : String -> Font +typeface = + Internal.Typeface + + +{-| -} +external : { url : String, name : String } -> Font +external { url, name } = + Internal.ImportFont name url + + +{-| Font sizes are always given as `px`. +-} +size : Int -> Attr decorative msg +size i = + Internal.StyleClass Flag.fontSize (Internal.FontSize i) + + +{-| In `px`. +-} +letterSpacing : Float -> Attribute msg +letterSpacing offset = + Internal.StyleClass Flag.letterSpacing <| + Internal.Single + ("ls-" ++ Internal.floatClass offset) + "letter-spacing" + (String.fromFloat offset ++ "px") + + +{-| In `px`. +-} +wordSpacing : Float -> Attribute msg +wordSpacing offset = + Internal.StyleClass Flag.wordSpacing <| + Internal.Single ("ws-" ++ Internal.floatClass offset) "word-spacing" (String.fromFloat offset ++ "px") + + +{-| Align the font to the left. +-} +alignLeft : Attribute msg +alignLeft = + Internal.Class Flag.fontAlignment classes.textLeft + + +{-| Align the font to the right. +-} +alignRight : Attribute msg +alignRight = + Internal.Class Flag.fontAlignment classes.textRight + + +{-| Center align the font. +-} +center : Attribute msg +center = + Internal.Class Flag.fontAlignment classes.textCenter + + +{-| -} +justify : Attribute msg +justify = + Internal.Class Flag.fontAlignment classes.textJustify + + + +-- {-| -} +-- justifyAll : Attribute msg +-- justifyAll = +-- Internal.class classesTextJustifyAll + + +{-| -} +underline : Attribute msg +underline = + Internal.htmlClass classes.underline + + +{-| -} +strike : Attribute msg +strike = + Internal.htmlClass classes.strike + + +{-| -} +italic : Attribute msg +italic = + Internal.htmlClass classes.italic + + +{-| -} +bold : Attribute msg +bold = + Internal.Class Flag.fontWeight classes.bold + + +{-| -} +light : Attribute msg +light = + Internal.Class Flag.fontWeight classes.textLight + + +{-| -} +hairline : Attribute msg +hairline = + Internal.Class Flag.fontWeight classes.textThin + + +{-| -} +extraLight : Attribute msg +extraLight = + Internal.Class Flag.fontWeight classes.textExtraLight + + +{-| -} +regular : Attribute msg +regular = + Internal.Class Flag.fontWeight classes.textNormalWeight + + +{-| -} +semiBold : Attribute msg +semiBold = + Internal.Class Flag.fontWeight classes.textSemiBold + + +{-| -} +medium : Attribute msg +medium = + Internal.Class Flag.fontWeight classes.textMedium + + +{-| -} +extraBold : Attribute msg +extraBold = + Internal.Class Flag.fontWeight classes.textExtraBold + + +{-| -} +heavy : Attribute msg +heavy = + Internal.Class Flag.fontWeight classes.textHeavy + + +{-| This will reset bold and italic. +-} +unitalicized : Attribute msg +unitalicized = + Internal.htmlClass classes.textUnitalicized + + +{-| -} +shadow : + { offset : ( Float, Float ) + , blur : Float + , color : Color + } + -> Attr decorative msg +shadow shade = + Internal.StyleClass Flag.txtShadows <| + Internal.Single (Internal.textShadowName shade) "text-shadow" (Internal.formatTextShadow shade) + + +{-| A glow is just a simplified shadow +-} +glow : Color -> Float -> Attr decorative msg +glow clr i = + let + shade = + { offset = ( 0, 0 ) + , blur = i * 2 + , color = clr + } + in + Internal.StyleClass Flag.txtShadows <| + Internal.Single (Internal.textShadowName shade) "text-shadow" (Internal.formatTextShadow shade) diff --git a/src/Element/Input.elm b/src/Element/Input.elm new file mode 100644 index 0000000..969abde --- /dev/null +++ b/src/Element/Input.elm @@ -0,0 +1,1834 @@ +module Element.Input + exposing + ( Label + , Option + , OptionState(..) + , Placeholder + , Thumb + , button + , checkbox + , currentPassword + , defaultCheckbox + , defaultThumb + , email + , focusedOnLoad + , labelAbove + , labelBelow + , labelLeft + , labelRight + , multiline + , newPassword + , option + , optionWith + , placeholder + , radio + , radioRow + , search + , slider + , spellChecked + , text + , thumb + , username + ) + +{-| + +@docs button + +@docs checkbox, defaultCheckbox + + +## Text Input + +@docs text, Placeholder, placeholder, username, newPassword, currentPassword, email, search, spellChecked + + +## Multiline Text + +@docs multiline + + +## Slider + +@docs slider, Thumb, thumb, defaultThumb + + +## Radio Buttons + +@docs radio, radioRow, Option, option, optionWith, OptionState + + +## Labels + +@docs Label, labelAbove, labelBelow, labelLeft, labelRight + +@docs focusedOnLoad + +-} + +import Element exposing (Attribute, Color, Element) +import Element.Background as Background +import Element.Border as Border +import Element.Events as Events +import Element.Font as Font +import Element.Region as Region +import Html +import Html.Attributes +import Html.Events +import Internal.Flag as Flag +import Internal.Model as Internal +import Internal.Style exposing (classes) +import Json.Decode as Json + + +{-| -} +type Placeholder msg + = Placeholder (List (Attribute msg)) (Element msg) + + +white = + Element.rgb 1 1 1 + + +darkGrey = + Element.rgb (186 / 255) (189 / 255) (182 / 255) + + +charcoal = + Element.rgb + (136 / 255) + (138 / 255) + (133 / 255) + + +{-| -} +placeholder : List (Attribute msg) -> Element msg -> Placeholder msg +placeholder = + Placeholder + + +{-| Every input has a required `label`. +-} +type Label msg + = Label Internal.Location (List (Attribute msg)) (Element msg) + + +{-| -} +labelRight : List (Attribute msg) -> Element msg -> Label msg +labelRight = + Label Internal.OnRight + + +{-| -} +labelLeft : List (Attribute msg) -> Element msg -> Label msg +labelLeft = + Label Internal.OnLeft + + +{-| -} +labelAbove : List (Attribute msg) -> Element msg -> Label msg +labelAbove = + Label Internal.Above + + +{-| -} +labelBelow : List (Attribute msg) -> Element msg -> Label msg +labelBelow = + Label Internal.Below + + +{-| A standard button. + +The `onPress` handler will be fired either `onClick` or when the element is focused and the enter key has been pressed. + + import Element.Input as Input + + Input.button [] + { onPress = Just ClickMsg + , label = text "My Button" + } + +`onPress` takes a `Maybe msg`. If you provide the value `Nothing`, then the button will be disabled. + +-} +button : + List (Attribute msg) + -> + { onPress : Maybe msg + , label : Element msg + } + -> Element msg +button attrs { onPress, label } = + Internal.element + Internal.asEl + -- We don't explicitly label this node as a button, + -- because buttons fire a bunch of times when you hold down the enter key. + -- We'd like to fire just once on the enter key, which means using keyup instead of keydown. + -- Because we have no way to disable keydown, though our messages get doubled. + Internal.div + (Element.width Element.shrink + :: Element.height Element.shrink + :: Internal.htmlClass classes.contentCenterX + :: Internal.htmlClass classes.contentCenterY + :: Internal.htmlClass classes.seButton + :: Element.pointer + :: focusDefault attrs + :: Internal.Describe Internal.Button + :: Internal.Attr (Html.Attributes.tabindex 0) + :: (case onPress of + Nothing -> + Internal.Attr (Html.Attributes.disabled True) :: attrs + + Just msg -> + Events.onClick msg + :: onEnter msg + :: attrs + ) + ) + (Internal.Unkeyed [ label ]) + + +focusDefault attrs = + if List.any hasFocusStyle attrs then + Internal.NoAttribute + else + Internal.htmlClass "focusable" + + +hasFocusStyle attr = + case attr of + Internal.StyleClass _ (Internal.PseudoSelector Internal.Focus _) -> + True + + _ -> + False + + +{-| -} +type alias Checkbox msg = + { onChange : Maybe (Bool -> msg) + , icon : Maybe (Element msg) + , checked : Bool + , label : Label msg + } + + +{-| -} +checkbox : + List (Attribute msg) + -> + { onChange : Bool -> msg + , icon : Bool -> Element msg + , checked : Bool + , label : Label msg + } + -> Element msg +checkbox attrs { label, icon, checked, onChange } = + let + attributes = + Element.spacing 6 + :: [ Internal.Attr (Html.Events.onClick (onChange (not checked))) + , Region.announce + , onKeyLookup <| + \code -> + if code == enter then + Just <| onChange (not checked) + else if code == space then + Just <| onChange (not checked) + else + Nothing + ] + ++ (tabindex 0 :: Element.pointer :: Element.alignLeft :: Element.width Element.fill :: attrs) + in + applyLabel attributes + label + (Internal.element + Internal.asEl + Internal.div + [ Internal.Attr <| + Html.Attributes.attribute "role" "checkbox" + , Internal.Attr <| + Html.Attributes.attribute "aria-checked" <| + if checked then + "true" + else + "false" + , Element.centerY + , Element.height Element.fill + , Element.width Element.shrink + ] + (Internal.Unkeyed + [ icon checked + ] + ) + ) + + +{-| -} +type Thumb + = Thumb (List (Attribute Never)) + + +{-| -} +thumb : List (Attribute Never) -> Thumb +thumb = + Thumb + + +{-| -} +defaultThumb : Thumb +defaultThumb = + Thumb + [ Element.width (Element.px 16) + , Element.height (Element.px 16) + , Border.rounded 8 + , Border.width 1 + , Border.color (Element.rgb 0.5 0.5 0.5) + , Background.color (Element.rgb 1 1 1) + ] + + +{-| A slider input, good for capturing float values. + + Input.slider + [ Element.height (Element.px 30) + -- Here is where we're creating/styling the "track" + , Element.behindContent + (Element.el + [ Element.width Element.fill + , Element.height (Element.px 2) + , Element.centerY + , Background.color grey + , Border.rounded 2 + ] + Element.none + ) + ] + { onChange = AdjustValue + , label = Input.labelAbove [] (text "My Slider Value") + , min = 0 + , max = 75 + , step = Nothing + , value = model.sliderValue + , thumb = + Input.defaultThumb + } + +The `thumb` is the icon that you can move around. + +The slider can be vertical or horizontal depending on the width/height of the slider. + + - `height fill` and `width (px someWidth)` will cause the slider to be vertical. + - `height (px someHeight)` and `width (px someWidth)` where `someHeight` > `someWidth` will also do it. + - otherwise, the slider will be horizontal. + +**Note:** If you want a slider for an `Int` value: + + - set `step` to be `Just 1`, or some other whole value + - `value = toFloat model.myInt` + - And finally, round the value before making a message `onChange = round >> AdjustValue` + +-} +slider : + List (Attribute msg) + -> + { onChange : Float -> msg + , label : Label msg + , min : Float + , max : Float + , value : Float + , thumb : Thumb + , step : Maybe Float + } + -> Element msg +slider attributes input = + let + (Thumb thumbAttributes) = + input.thumb + + width = + Internal.getWidth thumbAttributes + + height = + Internal.getHeight thumbAttributes + + vertical = + case ( trackWidth, trackHeight ) of + ( Nothing, Nothing ) -> + False + + ( Just (Internal.Px w), Just (Internal.Px h) ) -> + h > w + + ( Just (Internal.Px _), Just (Internal.Fill _) ) -> + True + + _ -> + False + + trackHeight = + Internal.getHeight attributes + + trackWidth = + Internal.getWidth attributes + + ( spacingX, spacingY ) = + Internal.getSpacing attributes ( 5, 5 ) + + factor = + (input.value - input.min) + / (input.max - input.min) + + {- Needed attributes + + Thumb Attributes + - Width/Height of thumb so that the input can shadow it. + + + Attributes + + OnParent -> + Spacing + + + On track -> + Everything else + + + + + The `` + + + -} + className = + "thmb-" ++ thumbWidthString ++ "-" ++ thumbHeightString + + thumbWidthString = + case width of + Nothing -> + "20px" + + Just (Internal.Px px) -> + String.fromInt px ++ "px" + + _ -> + "100%" + + thumbHeightString = + case height of + Nothing -> + "20px" + + Just (Internal.Px px) -> + String.fromInt px ++ "px" + + _ -> + "100%" + + thumbShadowStyle = + [ Internal.Property "width" + thumbWidthString + , Internal.Property "height" + thumbHeightString + ] + in + applyLabel + [ Element.spacingXY spacingX spacingY + , Region.announce + , Element.width Element.fill + ] + input.label + (Element.row + [ Element.width + (Maybe.withDefault Element.fill trackWidth) + , Element.height + (Maybe.withDefault (Element.px 20) trackHeight) + ] + [ Internal.element + Internal.asEl + (Internal.NodeName "input") + [ Internal.StyleClass Flag.active + (Internal.Style + ("input[type=\"range\"]." ++ className ++ "::-moz-range-thumb") + thumbShadowStyle + ) + , Internal.StyleClass Flag.hover + (Internal.Style + ("input[type=\"range\"]." ++ className ++ "::-webkit-slider-thumb") + thumbShadowStyle + ) + , Internal.StyleClass Flag.focus + (Internal.Style + ("input[type=\"range\"]." ++ className ++ "::-ms-thumb") + thumbShadowStyle + ) + , Internal.Attr (Html.Attributes.class (className ++ " focusable-parent")) + , Internal.Attr + (Html.Events.onInput + (\str -> + case String.toFloat str of + Nothing -> + -- This should never happen because the browser + -- should always provide a Float. + input.onChange 0 + + Just val -> + input.onChange val + ) + ) + , Internal.Attr <| + Html.Attributes.type_ "range" + , Internal.Attr <| + Html.Attributes.step + (case input.step of + Nothing -> + -- Note: If we set `any` here, + -- Firefox makes a single press of the arrows keys equal to 1 + -- We could set the step manually to the effective range / 100 + -- String.fromFloat ((input.max - input.min) / 100) + -- Which matches Chrome's default behavior + -- HOWEVER, that means manually moving a slider with the mouse will snap to that interval. + "any" + + Just step -> + String.fromFloat step + ) + , Internal.Attr <| + Html.Attributes.min (String.fromFloat input.min) + , Internal.Attr <| + Html.Attributes.max (String.fromFloat input.max) + , Internal.Attr <| + Html.Attributes.value (String.fromFloat input.value) + , if vertical then + Internal.Attr <| + Html.Attributes.attribute "orient" "vertical" + else + Internal.NoAttribute + , Element.width <| + if vertical then + Maybe.withDefault (Element.px 20) trackHeight + else + Maybe.withDefault Element.fill trackWidth + , Element.height <| + if vertical then + Maybe.withDefault Element.fill trackWidth + else + Maybe.withDefault (Element.px 20) trackHeight + ] + (Internal.Unkeyed []) + , Element.el + (Element.width + (Maybe.withDefault Element.fill trackWidth) + :: Element.height + (Maybe.withDefault (Element.px 20) trackHeight) + :: attributes + -- This is after `attributes` because the thumb should be in front of everything. + ++ [ Element.behindContent <| + if vertical then + viewVerticalThumb factor thumbAttributes trackWidth + else + viewHorizontalThumb factor thumbAttributes trackHeight + ] + ) + Element.none + ] + ) + + +viewHorizontalThumb factor thumbAttributes trackHeight = + Element.row + [ Element.width Element.fill + , Element.height (Maybe.withDefault Element.fill trackHeight) + , Element.centerY + ] + [ Element.el + [ Element.width (Element.fillPortion (round <| factor * 10000)) + ] + Element.none + , Element.el + (Element.centerY + :: List.map (Internal.mapAttr Basics.never) thumbAttributes + ) + Element.none + , Element.el + [ Element.width (Element.fillPortion (round <| (abs <| 1 - factor) * 10000)) + ] + Element.none + ] + + +viewVerticalThumb factor thumbAttributes trackWidth = + Element.column + [ Element.height Element.fill + , Element.width (Maybe.withDefault Element.fill trackWidth) + , Element.centerX + ] + [ Element.el + [ Element.height (Element.fillPortion (round <| (abs <| 1 - factor) * 10000)) + ] + Element.none + , Element.el + (Element.centerX + :: List.map (Internal.mapAttr Basics.never) thumbAttributes + ) + Element.none + , Element.el + [ Element.height (Element.fillPortion (round <| factor * 10000)) + ] + Element.none + ] + + +type alias TextInput = + { type_ : TextKind + , spellchecked : Bool + , autofill : Maybe String + } + + +type TextKind + = TextInputNode String + | TextArea + + +{-| -} +type alias Text msg = + { onChange : String -> msg + , text : String + , placeholder : Maybe (Placeholder msg) + , label : Label msg + } + + +type Padding + = Padding Int Int Int Int + + +{-| + + attributes + + + attribute::width/height fill + attribtue::alignment + attribute::spacing + attribute::fontsize/family/lineheight + + attribute::nearby(placeholder) + attribute::width/height fill + inFront -> + placeholder + attribute::padding + + + + textarea -> + special height for height-content + attribtue::padding + attribute::lineHeight + attributes +