diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8b631e7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+# elm-package generated files
+elm-stuff
+# elm-repl generated files
+repl-temp-*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..362fa72
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2019, Orasund
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 4e407d4..dd3c3a2 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,15 @@
-# elm-widgets
-Collection of reusable views for elm-ui.
+# Elm-Ui-Widgets
+
+Usefull Widgets written in for Elm-ui.
+These include:
+
+* Select
+* Multi Select
+* Collapsable
+* Dialog
+* Carousel
+* Snackbar
+* Sort Table
+* Filter Select
+* Validated Input
+* Scrolling Nav
\ No newline at end of file
diff --git a/docs.json b/docs.json
new file mode 100644
index 0000000..89b845a
--- /dev/null
+++ b/docs.json
@@ -0,0 +1 @@
+[{"name":"Framework","comment":" This module includes the basic building bocks.\r\nMaybe start by copying the follow code into your project:\r\n\r\n```\r\nview : Html msg\r\nview =\r\n Framework.layout [] <|\r\n Element.el Framework.container <|\r\n Element.text \"the first element should be a container.\"\r\n```\r\n\r\n@docs layout, container, layoutOptions, layoutAttributes\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"container","comment":" The container should be the outer most element.\r\nIt centers the content and sets the background to white.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"layout","comment":" A replacement of Element.layout adding both the Framework.layoutOptions and the Framework.layoutAttributes.\r\n","type":"List.List (Element.Attribute msg) -> Element.Element msg -> Html.Html msg"},{"name":"layoutAttributes","comment":" The default Attributes. Check the source code if you want to know more.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"layoutOptions","comment":" The default layoutOptions. Check the source code if you want to know more.\r\n","type":"List.List Element.Option"}],"binops":[]},{"name":"Framework.Button","comment":" This module contains a attribute to style buttons.\r\n\r\n```\r\nInput.button (Button.simple ++ Color.primary) <|\r\n { onPress = Nothing\r\n , label = Element.text \"Button.simple ++ Color.primary\"\r\n }\r\n```\r\n\r\nThe attribute can only be used on `Input.button` but it may be with additional attibutes from this package.\r\n\r\n@docs simple\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"simple","comment":" a simple Button styling. Check the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Card","comment":" The Card attributes can be used almost anywere in the elm-ui elements.\r\n\r\nHere are some examples:\r\n\r\n```\r\nElement.column (Card.simple ++ Grid.simple) <|\r\n [ Element.text <| \"adds a border around the column\"\r\n ]\r\n```\r\n\r\n```\r\nElement.el Card.small <|\r\n Element.text \"a basic card\"\r\n```\r\n\r\n```\r\nInput.button (Button.simple ++ Card.large) <|\r\n { onPress = Nothing\r\n , label = Element.text \"a clickable card\"\r\n }\r\n```\r\n\r\n@docs simple, small, large, fill\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"fill","comment":" A card filling all the avaiable space.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"large","comment":" A 480px wide card.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"simple","comment":" A basic card.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"small","comment":" A 240px wide card.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Color","comment":" This module contains the colors used in the framework.\r\n\r\n@docs cyan, green, lighterGrey, lightGrey, grey, darkGrey, darkerGrey, red, turquoise, yellow\r\n\r\nSome colors also have a Attribute that can be used nearly everywhere.\r\n\r\n@docs danger, light, dark, disabled, info, primary, success, warning\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"cyan","comment":" ","type":"Element.Color"},{"name":"danger","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"dark","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"darkGrey","comment":" ","type":"Element.Color"},{"name":"darkerGrey","comment":" ","type":"Element.Color"},{"name":"disabled","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"green","comment":" ","type":"Element.Color"},{"name":"grey","comment":" ","type":"Element.Color"},{"name":"info","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"light","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"lightGrey","comment":" ","type":"Element.Color"},{"name":"lighterGrey","comment":" ","type":"Element.Color"},{"name":"primary","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"red","comment":" ","type":"Element.Color"},{"name":"success","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"turquoise","comment":" ","type":"Element.Color"},{"name":"warning","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"yellow","comment":" ","type":"Element.Color"}],"binops":[]},{"name":"Framework.Grid","comment":" This module include the basic attributes for columns and rows and two variants.\r\nAny of these Attributes can be used for columns and rows.\r\n\r\n```\r\nElement.row Grid.spacedEvenly <|\r\n [ Element.text \"left item\"\r\n , Element.text \"center item\"\r\n , Element.text \"right item\"\r\n ]\r\n```\r\n\r\n@docs simple, spacedEvenly, section\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"section","comment":" The simple attributes but with a horizontal line at the top\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"simple","comment":" The basic attributes for columns and rows.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"spacedEvenly","comment":" The simple attibutes but with evenly spaced elements.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Heading","comment":" Styling for heading\r\n\r\n```\r\nElement.el Heading.h1 <| Element.text \"Only Element.el may be styled as a heading\"\r\n```\r\n\r\n@docs h1, h2, h3, h4, h5, h6\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"h1","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h2","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h3","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h4","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h5","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h6","comment":" ","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Input","comment":" This module exposes simple attibutes for Inputs (beside Buttons) and\r\nstyling for labels.\r\n\r\n```\r\nInput.text Input.simple\r\n { onChange = always ()\r\n , text = \"Input.simple\"\r\n , placeholder = Nothing\r\n , label = Input.labelLeft Input.label <| Element.text \"Input.label\"\r\n }\r\n```\r\n\r\n@docs simple, label\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"label","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"simple","comment":" ","type":"List.List (Element.Attribute msg)"}],"binops":[]}]
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..448753b
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,13358 @@
+
+
+
+
+ Example
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/select.png b/docs/select.png
new file mode 100644
index 0000000..b544c66
Binary files /dev/null and b/docs/select.png differ
diff --git a/elm.json b/elm.json
new file mode 100644
index 0000000..3d7f294
--- /dev/null
+++ b/elm.json
@@ -0,0 +1,29 @@
+{
+ "type": "package",
+ "name": "Orasund/elm-ui-widgets",
+ "summary": "Collection of reusable views for elm-ui.",
+ "license": "BSD-3-Clause",
+ "version": "1.0.0",
+ "exposed-modules": [
+ "View",
+ "View.FilterSelect",
+ "View.ValidatedInput",
+ "View.WrappedColumn"
+ ],
+ "elm-version": "0.19.0 <= v < 0.20.0",
+ "dependencies": {
+ "Orasund/elm-ui-framework": "1.6.1 <= v < 2.0.0",
+ "elm/browser": "1.0.2 <= v < 2.0.0",
+ "elm/core": "1.0.0 <= v < 2.0.0",
+ "elm/html": "1.0.0 <= v < 2.0.0",
+ "elm/time": "1.0.0 <= v < 2.0.0",
+ "elm-community/intdict": "3.0.0 <= v < 4.0.0",
+ "jasonliang512/elm-heroicons": "1.0.2 <= v < 2.0.0",
+ "mdgriffith/elm-ui": "1.1.5 <= v < 2.0.0",
+ "turboMaCk/queue": "1.0.2 <= v < 2.0.0",
+ "wernerdegroot/listzipper": "4.0.0 <= v < 5.0.0"
+ },
+ "test-dependencies": {
+ "elm-explorations/test": "1.2.1 <= v < 2.0.0"
+ }
+}
diff --git a/example/elm.json b/example/elm.json
new file mode 100644
index 0000000..a891a32
--- /dev/null
+++ b/example/elm.json
@@ -0,0 +1,32 @@
+{
+ "type": "application",
+ "source-directories": [
+ "src",
+ "../src"
+ ],
+ "elm-version": "0.19.1",
+ "dependencies": {
+ "direct": {
+ "Orasund/elm-ui-framework": "1.6.1",
+ "elm/browser": "1.0.2",
+ "elm/core": "1.0.5",
+ "elm/html": "1.0.0",
+ "elm/time": "1.0.0",
+ "elm-community/intdict": "3.0.0",
+ "jasonliang512/elm-heroicons": "1.0.2",
+ "mdgriffith/elm-ui": "1.1.5",
+ "turboMaCk/queue": "1.0.2",
+ "wernerdegroot/listzipper": "4.0.0"
+ },
+ "indirect": {
+ "elm/json": "1.1.3",
+ "elm/svg": "1.0.1",
+ "elm/url": "1.0.0",
+ "elm/virtual-dom": "1.0.2"
+ }
+ },
+ "test-dependencies": {
+ "direct": {},
+ "indirect": {}
+ }
+}
diff --git a/example/src/Component.elm b/example/src/Component.elm
new file mode 100644
index 0000000..9f50690
--- /dev/null
+++ b/example/src/Component.elm
@@ -0,0 +1,179 @@
+module Component exposing (Model,Msg(..),init,update,view)
+
+import Browser
+import Element exposing (Element,Color)
+import Element.Input as Input
+import Element.Background as Background
+import Framework
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Grid as Grid
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Set exposing (Set)
+import Widget
+import Widget.FilterSelect as FilterSelect
+import Widget.ScrollingNav as ScrollingNav
+import Widget.ValidatedInput as ValidatedInput
+import Widget.Snackbar as Snackbar
+import Time
+import Heroicons.Solid as Heroicons
+
+type alias Model =
+ { filterSelect : FilterSelect.Model
+ , validatedInput : ValidatedInput.Model () (( String, String ))
+ }
+
+type Msg
+ = FilterSelectSpecific FilterSelect.Msg
+ | ValidatedInputSpecific ValidatedInput.Msg
+
+
+init : Model
+init =
+
+ { filterSelect =
+ [ "Apple"
+ , "Kiwi"
+ , "Strawberry"
+ , "Pineapple"
+ , "Mango"
+ , "Grapes"
+ , "Watermelon"
+ , "Orange"
+ , "Lemon"
+ , "Blueberry"
+ , "Grapefruit"
+ , "Coconut"
+ , "Cherry"
+ , "Banana"
+ ]
+ |> Set.fromList
+ |> FilterSelect.init
+ , validatedInput =
+ ValidatedInput.init
+ { value = ("John","Doe")
+ , validator =
+ \string ->
+ case string |> String.split " " of
+ [ first, second ] ->
+ Ok (( first, second ))
+
+ _ ->
+ Err ()
+ , toString =
+ (\( first, second ) -> first ++ " " ++ second)
+ }
+ }
+
+
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+ case msg of
+ FilterSelectSpecific m ->
+ ( { model
+ | filterSelect = model.filterSelect |> FilterSelect.update m
+ }
+ , Cmd.none
+ )
+
+ ValidatedInputSpecific m ->
+ ( { model
+ | validatedInput = model.validatedInput |> ValidatedInput.update m
+ }
+ , Cmd.none
+ )
+
+filterSelect : FilterSelect.Model -> Element Msg
+filterSelect model =
+ Element.column (Grid.simple ++ Card.small) <|
+ [ Element.el Heading.h3 <| Element.text "Filter Select"
+ , case model.selected of
+ Just selected ->
+ Element.row Grid.compact
+ [ Element.el (Tag.simple ++ Group.left) <| Element.text <| selected
+ , Input.button (Tag.simple ++ Group.right ++ Color.danger)
+ { onPress = Just <| FilterSelectSpecific <| FilterSelect.Selected Nothing
+ , label = Element.html <| Heroicons.x [Attributes.width 16]
+ }
+ ]
+
+ Nothing ->
+ Element.column Grid.simple
+ [ FilterSelect.viewInput Input.simple
+ model
+ { msgMapper = FilterSelectSpecific
+ , placeholder = Just <| Input.placeholder [] <| Element.text <|
+ "Fruit"
+ , label = "Fruit"
+ }
+ , model
+ |> FilterSelect.viewOptions
+ |> List.map
+ (\string ->
+ Input.button (Button.simple ++ Tag.simple)
+ { onPress = Just <| FilterSelectSpecific <| FilterSelect.Selected <| Just <| string
+ , label = Element.text string
+ }
+ )
+ |> Element.wrappedRow [ Element.spacing 10 ]
+ ]
+
+ ]
+
+
+validatedInput : ValidatedInput.Model () ( ( String, String )) -> Element Msg
+validatedInput model =
+ Element.column (Grid.simple ++ Card.small) <|
+ [ Element.el Heading.h3 <| Element.text "Validated Input"
+ , ValidatedInput.view Input.simple
+ model
+ { label = "First Name, Sir Name"
+ , msgMapper = ValidatedInputSpecific
+ , placeholder = Nothing
+ , readOnly = \maybeTuple ->
+ Element.row Grid.compact
+ [ maybeTuple
+ |> (\(a,b) -> a ++ " " ++ b)
+ |> Element.text
+
+ |> Element.el (Tag.simple ++ Group.left)
+ , Heroicons.pencil [Attributes.width 16]
+ |> Element.html
+ |>Element.el (Tag.simple ++ Group.right ++ Color.primary)
+ ]
+ }
+ ]
+
+
+
+
+scrollingNavCard : Element msg
+scrollingNavCard =
+ [Element.el Heading.h3 <| Element.text "Scrolling Nav"
+ , Element.text "Resize the screen and use the scrollbar to see the scrolling navigation in action."
+ |> List.singleton
+ |> Element.paragraph []
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+view : Model -> Element Msg
+view model =
+ Element.column (Grid.section ++ [Element.centerX])
+ [ Element.el Heading.h2 <| Element.text "Components"
+ , "Components have a Model, an Update- and sometimes even a Subscription-function. It takes some time to set them up correctly."
+ |> Element.text
+ |> List.singleton
+ |> Element.paragraph []
+ , Element.wrappedRow (Grid.simple ++ [Element.centerX])
+ <|
+ [ filterSelect model.filterSelect
+ , validatedInput model.validatedInput
+ , scrollingNavCard
+ ]
+ ]
\ No newline at end of file
diff --git a/example/src/Example.elm b/example/src/Example.elm
new file mode 100644
index 0000000..03c4b57
--- /dev/null
+++ b/example/src/Example.elm
@@ -0,0 +1,274 @@
+module Example exposing (main)
+
+import Browser
+import Element exposing (Element)
+import Element.Input as Input
+import Framework
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Grid as Grid
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Set exposing (Set)
+import Widget
+import Widget.FilterSelect as FilterSelect
+import Widget.ScrollingNav as ScrollingNav
+import Widget.ValidatedInput as ValidatedInput
+import Widget.Snackbar as Snackbar
+import Stateless
+import Reusable
+import Component
+import Time
+
+type Section
+ = ComponentViews
+ | ReusableViews
+ | StatelessViews
+
+type alias Model =
+ { component : Component.Model
+ , stateless : Stateless.Model
+ , reusable : Reusable.Model
+ , scrollingNav : ScrollingNav.Model Section
+ , snackbar : Snackbar.Model String
+ , displayDialog : Bool
+ }
+
+type Msg
+ = StatelessSpecific Stateless.Msg
+ | ReusableSpecific Reusable.Msg
+ | ComponentSpecific Component.Msg
+ | ScrollingNavSpecific (ScrollingNav.Msg Section)
+ | TimePassed Int
+ | AddSnackbar String
+ | ToggleDialog Bool
+
+
+init : () -> ( Model, Cmd Msg )
+init () =
+ let
+ ( scrollingNav, cmd ) =
+ ScrollingNav.init
+ { labels =
+ \section ->
+ case section of
+ ComponentViews ->
+ "Component Views"
+ ReusableViews ->
+ "Reusable Views"
+
+ StatelessViews ->
+ "Stateless Views"
+ , arrangement = [ StatelessViews, ReusableViews, ComponentViews ]
+ }
+ in
+ ({ component = Component.init
+ , stateless = Stateless.init
+ , reusable = Reusable.init
+ , scrollingNav = scrollingNav
+ , snackbar = Snackbar.init
+ , displayDialog = False
+ }
+
+ , cmd |> Cmd.map ScrollingNavSpecific
+ )
+
+
+
+
+view : Model -> Html Msg
+view model =
+ [ Element.el [ Element.height <| Element.px <| 42 ] <| Element.none
+ , [ Element.el Heading.h1 <| Element.text "Elm-Ui-Widgets"
+ , model.scrollingNav
+ |> ScrollingNav.view
+ (\section ->
+ case section of
+ ComponentViews ->
+ model.component
+ |> Component.view
+ |> Element.map ComponentSpecific
+ ReusableViews ->
+ Reusable.view
+ {addSnackbar = AddSnackbar
+ ,model = model.reusable
+ ,msgMapper = ReusableSpecific
+ }
+
+ StatelessViews ->
+ Stateless.view
+ { msgMapper = StatelessSpecific
+ , showDialog = ToggleDialog True
+ }
+ model.stateless
+ )
+ ]
+ |> Element.column Framework.container
+ ]
+ |> Element.column Grid.compact
+ |> Framework.responsiveLayout
+ [ Element.inFront <|
+ (model.scrollingNav
+ |> ScrollingNav.viewSections
+ { fromString =
+ \string ->
+ case string of
+ "Component Views" ->
+ Just ComponentViews
+ "Reusable Views" ->
+ Just ReusableViews
+
+ "Stateless Views" ->
+ Just StatelessViews
+
+ _ ->
+ Nothing
+ , label = Element.text
+ , msgMapper = ScrollingNavSpecific
+ }
+ |> Widget.select
+ |> List.map (\(config,selected) ->
+ Input.button (Button.simple
+ ++ Group.center
+ ++ (if selected then
+ Color.primary
+
+ else
+ Color.dark
+ ))
+ config
+ )
+ |> Element.row
+ (Color.dark ++ Grid.compact)
+ |> Element.el
+ (Framework.container
+ ++ [ Element.padding <| 0
+ , Element.centerX
+ ]
+ )
+ |> Element.el
+ (Color.dark
+ ++ [ Element.alignTop
+ , Element.height <| Element.px <| 42
+ , Element.width <| Element.fill
+ ]
+ )
+ )
+ , Element.inFront <|
+ ( model.snackbar
+ |> Snackbar.current
+ |> Maybe.map
+ (Element.text >>
+ List.singleton >>
+ Element.paragraph (Card.simple ++ Color.dark)
+ >> Element.el [Element.padding 8,Element.alignBottom
+ , Element.alignRight]
+
+
+ )
+ |> Maybe.withDefault Element.none
+ )
+ , Element.inFront <|
+ if model.displayDialog then
+ Widget.dialog {
+ onDismiss = Just <| ToggleDialog False
+ ,content =
+ [ Element.el Heading.h3 <| Element.text "Dialog"
+ , "This is a dialog window"
+ |> Element.text
+ |> List.singleton
+ |> Element.paragraph []
+ , Input.button (Button.simple ++ [Element.alignRight])
+ {onPress = Just <| ToggleDialog False
+ , label = Element.text "Ok"
+ }
+ ]
+ |>Element.column (Grid.simple ++ Card.large)
+ } else Element.none
+ ]
+
+
+
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+ case msg of
+ ComponentSpecific m ->
+ model.component
+ |> Component.update m
+ |> Tuple.mapBoth
+ (\component ->
+ { model
+ | component = component
+ }
+ )
+ (Cmd.map ComponentSpecific)
+
+ ReusableSpecific m ->
+ (model.reusable
+ |> Reusable.update m
+ |> (\reusable ->
+ { model
+ | reusable = reusable
+ }
+ ),Cmd.none)
+
+ StatelessSpecific m ->
+ model.stateless
+ |> Stateless.update m
+ |> Tuple.mapBoth
+ (\stateless ->
+ { model
+ | stateless = stateless
+ }
+ )
+ (Cmd.map StatelessSpecific)
+
+
+ ScrollingNavSpecific m ->
+ model.scrollingNav
+ |> ScrollingNav.update m
+ |> Tuple.mapBoth
+ (\scrollingNav ->
+ { model
+ | scrollingNav = scrollingNav
+ }
+ )
+ (Cmd.map ScrollingNavSpecific)
+
+ TimePassed int ->
+ ({ model
+ | snackbar = model.snackbar |> Snackbar.timePassed int
+ },Cmd.none)
+
+ AddSnackbar string ->
+ ( { model | snackbar = model.snackbar |> Snackbar.insert string}
+ , Cmd.none
+ )
+
+ ToggleDialog bool ->
+ ( { model | displayDialog = bool }
+ , Cmd.none
+ )
+
+subscriptions : Model -> Sub Msg
+subscriptions model=
+ Sub.batch
+ [ScrollingNav.subscriptions
+ |> Sub.map ScrollingNavSpecific
+ , Time.every 500 (always ( TimePassed 500))
+ ]
+
+
+main : Program () Model Msg
+main =
+ Browser.element
+ { init = init
+ , view = view
+ , update = update
+ , subscriptions = subscriptions
+ }
diff --git a/example/src/Reusable.elm b/example/src/Reusable.elm
new file mode 100644
index 0000000..defe47d
--- /dev/null
+++ b/example/src/Reusable.elm
@@ -0,0 +1,144 @@
+module Reusable exposing (Model,Msg,init,view,update)
+
+import Browser
+import Element exposing (Element,Color)
+import Element.Input as Input
+import Element.Background as Background
+import Framework
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Grid as Grid
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Set exposing (Set)
+import Widget
+import Widget.FilterSelect as FilterSelect
+import Widget.ScrollingNav as ScrollingNav
+import Widget.ValidatedInput as ValidatedInput
+import Widget.Snackbar as Snackbar
+import Widget.SortTable as SortTable
+import Time
+import Heroicons.Solid as Heroicons
+import Element.Font as Font
+
+type alias Model =
+ SortTable.Model
+
+type Msg =
+ SortBy {title : String, asc : Bool }
+
+type alias Item =
+ {name : String
+ ,amount : Int
+ ,price : Float
+ }
+
+update : Msg -> Model -> Model
+update msg model =
+ case msg of
+ SortBy m ->
+ m
+
+init : Model
+init =
+ SortTable.sortBy {title="Name",asc=True}
+
+snackbar : (String -> msg) -> Element msg
+snackbar addSnackbar =
+ [ Element.el Heading.h3 <| Element.text "Snackbar"
+ , Input.button Button.simple
+ { onPress = Just <| addSnackbar "This is a notification. It will disappear after 10 seconds."
+ , label = "Add Notification"
+ |> Element.text
+ |> List.singleton
+ |> Element.paragraph []
+ }
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+sortTable : SortTable.Model -> Element Msg
+sortTable model =
+ [ Element.el Heading.h3 <| Element.text "Sort Table"
+ , SortTable.view
+ { content =
+ [ {id = 1, name = "Antonio", rating = 2.456}
+ , {id = 2, name = "Ana", rating = 1.34}
+ , {id = 3, name = "Alfred", rating = 4.22}
+ , {id = 4, name = "Thomas", rating = 3 }
+ ]
+ , columns =
+ [ SortTable.intColumn
+ { title = "Id"
+ , value = .id
+ , toString = \int -> "#" ++ String.fromInt int
+ }
+ , SortTable.stringColumn
+ { title = "Name"
+ , value = .name
+ , toString = identity
+ }
+ , SortTable.floatColumn
+ { title = "rating"
+ , value = .rating
+ , toString = String.fromFloat
+ }
+ ]
+ , model = model
+ }
+ |> (\{data,columns} ->
+ {data = data
+ ,columns = columns
+ |> List.map (\config->
+ { header =
+ Input.button [Font.bold]
+ { onPress = {title = config.header
+ ,asc = if config.header == model.title then
+ not model.asc
+ else
+ True
+ }|> SortBy |> Just
+ , label =
+ if config.header == model.title then
+ [config.header |> Element.text
+ , Element.html <|if model.asc then
+ Heroicons.cheveronUp [Attributes.width 16]
+
+ else
+ Heroicons.cheveronDown [Attributes.width 16]
+ ]
+ |> Element.row (Grid.simple ++ [Font.bold])
+ else
+
+ config.header |> Element.text
+ }
+
+ , view = config.view >> Element.text
+ , width = Element.fill
+ }
+ )
+ })
+ |> Element.table Grid.simple
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+view : { addSnackbar : String -> msg
+ , msgMapper : Msg -> msg
+ , model : Model } -> Element msg
+view {addSnackbar,msgMapper,model} =
+ Element.column (Grid.section ++ [Element.centerX])
+ [ Element.el Heading.h2 <| Element.text "Reusable Views"
+ , "Reusable views have an internal state but no update function. You will need to do some wiring, but nothing complicated."
+ |> Element.text
+ |> List.singleton
+ |> Element.paragraph []
+ , Element.wrappedRow (Grid.simple ++ [Element.centerX])
+ <|
+ [ snackbar addSnackbar
+ , sortTable model |> Element.map msgMapper
+ ]
+ ]
\ No newline at end of file
diff --git a/example/src/Stateless.elm b/example/src/Stateless.elm
new file mode 100644
index 0000000..8eb707b
--- /dev/null
+++ b/example/src/Stateless.elm
@@ -0,0 +1,219 @@
+module Stateless exposing (Model,Msg,init,update,view)
+
+import Element exposing (Element)
+import Element.Input as Input
+import Element.Background as Background
+import Set exposing (Set)
+import Framework.Grid as Grid
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Heroicons.Solid as Heroicons
+import Widget
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Array exposing (Array)
+
+type alias Model =
+ { selected : Maybe Int
+ , multiSelected : Set Int
+ , isCollapsed : Bool
+ , carousel : Int
+ }
+
+type Msg =
+ ChangedSelected Int
+ | ChangedMultiSelected Int
+ | ToggleCollapsable Bool
+ | SetCarousel Int
+
+init : Model
+init =
+ { selected = Nothing
+ , multiSelected = Set.empty
+ , isCollapsed = False
+ , carousel = 0
+ }
+
+update : Msg -> Model -> (Model,Cmd Msg)
+update msg model =
+ case msg of
+ ChangedSelected int ->
+ ( { model
+ | selected = Just int
+ }
+ , Cmd.none
+ )
+
+ ChangedMultiSelected int ->
+ ( { model
+ | multiSelected =
+ model.multiSelected |>
+ if model.multiSelected |> Set.member int then
+ Set.remove int
+ else
+ Set.insert int
+ }
+ , Cmd.none )
+
+ ToggleCollapsable bool ->
+ ( { model
+ | isCollapsed = bool
+ }
+ , Cmd.none)
+
+ SetCarousel int ->
+ ( if (int < 0) || (int > 3) then
+ model
+ else
+ {model
+ | carousel = int
+ }
+ , Cmd.none
+ )
+
+select : Model -> Element Msg
+select model =
+ [ Element.el Heading.h3 <| Element.text "Select"
+ , Widget.select
+ { selected = model.selected
+ , options = [ 1, 2, 42 ]
+ , label = String.fromInt >> Element.text
+ , onChange = ChangedSelected
+ }
+ |> List.indexedMap (\i (config,selected)->
+ Input.button
+ (Button.simple
+ ++ (if i == 0 then
+ Group.left
+ else if i == 2 then
+ Group.right
+ else
+ Group.center)
+ ++ (if selected then
+ Color.primary
+
+ else
+ []
+ )
+ )
+ config
+ )
+ |> Element.row Grid.compact
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+multiSelect : Model -> Element Msg
+multiSelect model =
+ [ Element.el Heading.h3 <| Element.text "Multi Select"
+ , Widget.multiSelect
+ { selected = model.multiSelected
+ , options = [ 1, 2, 42 ]
+ , label = String.fromInt >> Element.text
+ , onChange = ChangedMultiSelected
+ }
+ |> List.indexedMap (\i (config,selected)->
+ Input.button
+ (Button.simple
+ ++ (if i == 0 then
+ Group.left
+ else if i == 2 then
+ Group.right
+ else
+ Group.center)
+ ++ (if selected then
+ Color.primary
+
+ else
+ []
+ )
+ )
+ config
+ )
+ |> Element.row Grid.compact
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+
+collapsable : Model -> Element Msg
+collapsable model =
+ [Element.el Heading.h3 <| Element.text "Collapsable"
+ ,Widget.collapsable
+ {onToggle = ToggleCollapsable
+ ,isCollapsed = model.isCollapsed
+ ,label = Element.row Grid.compact
+ [ Element.html <|
+ if model.isCollapsed then
+ Heroicons.cheveronRight [ Attributes.width 20]
+ else
+ Heroicons.cheveronDown [ Attributes.width 20]
+ , Element.el Heading.h4 <|Element.text <| "Title"
+ ]
+ ,content = Element.text <| "Hello World"
+ }
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+dialog : msg -> Model -> Element msg
+dialog showDialog model =
+ [ Element.el Heading.h3 <| Element.text "Dialog"
+ , Input.button Button.simple
+ { onPress = Just showDialog
+ , label = Element.text <| "Show Dialog"
+ }
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+carousel : Model -> Element Msg
+carousel model =
+ [ Element.el Heading.h3 <| Element.text "Carousel"
+ , Widget.carousel
+ {content = (Color.cyan,[Color.yellow, Color.green , Color.red ]|> Array.fromList)
+ ,current = model.carousel
+ , label = \c ->
+ [ Input.button [Element.centerY]
+ { onPress = Just <| SetCarousel <| model.carousel - 1
+ , label = Heroicons.cheveronLeft [Attributes.width 20]
+ |> Element.html
+ }
+ , Element.el
+ (Card.simple
+ ++ [ Background.color <| c
+ , Element.height <| Element.px <| 100
+ , Element.width <| Element.px <| 100
+ ]
+ ) <| Element.none
+ , Input.button [Element.centerY]
+ { onPress = Just <| SetCarousel <| model.carousel + 1
+ , label = Heroicons.cheveronRight [Attributes.width 20]
+ |> Element.html
+ }
+ ]
+ |> Element.row (Grid.simple ++ [Element.centerX, Element.width<| Element.shrink])
+ }
+ ]
+ |> Element.column (Grid.simple ++ Card.small)
+
+
+view : { msgMapper : Msg -> msg, showDialog : msg} -> Model -> Element msg
+view {msgMapper,showDialog} model =
+ Element.column (Grid.section)
+ [ Element.el Heading.h2 <| Element.text "Stateless Views"
+ , "Stateless views are simple functions that view some content. No wiring required."
+ |> Element.text
+ |> List.singleton
+ |> Element.paragraph []
+ , Element.wrappedRow
+ Grid.simple
+ <|
+ [ select model |> Element.map msgMapper
+ , multiSelect model |> Element.map msgMapper
+ , collapsable model |> Element.map msgMapper
+ , dialog showDialog model
+ , carousel model |> Element.map msgMapper
+ ]
+ ]
\ No newline at end of file
diff --git a/src/Widget.elm b/src/Widget.elm
new file mode 100644
index 0000000..165e1fa
--- /dev/null
+++ b/src/Widget.elm
@@ -0,0 +1,247 @@
+module Widget exposing (select, multiSelect, collapsable, carousel, dialog)
+
+{-| This module contains functions for displaying data.
+
+@docs select, multiSelect, collapsable, carousel, dialog
+
+-}
+
+import Array exposing (Array)
+import Element exposing (Element)
+import Element.Background as Background
+import Element.Events as Events
+import Element.Input as Input
+import Set exposing (Set)
+
+
+{-| Selects one out of multiple options. This can be used for radio buttons or Menus.
+
+```
+ Widget.select
+ { selected = model.selected
+ , options = [ 1, 2, 42 ]
+ , label = String.fromInt >> Element.text
+ , onChange = ChangedSelected
+ }
+ |> List.map (\(config,selected)->
+ Input.button (if selected then [Font.bold] else []) config
+ )
+ |> Element.row []
+```
+
+-}
+select :
+ { selected : Maybe a
+ , options : List a
+ , label : a -> Element msg
+ , onChange : a -> msg
+ }
+ ->
+ List
+ ( { label : Element msg
+ , onPress : Maybe msg
+ }
+ , Bool
+ )
+select { selected, options, label, onChange } =
+ options
+ |> List.map
+ (\a ->
+ ( { onPress = a |> onChange |> Just
+ , label = label a
+ }
+ , selected == Just a
+ )
+ )
+
+
+{-| Selects multible options. This can be used for checkboxes.
+
+```
+ Widget.multiSelect
+ { selected = model.multiSelected
+ , options = [ 1, 2, 42 ]
+ , label = String.fromInt >> Element.text
+ , onChange = ChangedMultiSelected
+ }
+ |> List.map (\(config,selected)->
+ Input.button
+ (if selected then
+ [Font.bold]
+
+ else
+ []
+ )
+ config
+ )
+ |> Element.row []
+```
+
+-}
+multiSelect :
+ { selected : Set comparable
+ , options : List comparable
+ , label : comparable -> Element msg
+ , onChange : comparable -> msg
+ }
+ ->
+ List
+ ( { label : Element msg
+ , onPress : Maybe msg
+ }
+ , Bool
+ )
+multiSelect { selected, options, label, onChange } =
+ options
+ |> List.map
+ (\a ->
+ ( { onPress = a |> onChange |> Just
+ , label =
+ label a
+ }
+ , selected |> Set.member a
+ )
+ )
+
+
+{-| Some collapsable content.
+
+```
+ Widget.collapsable
+ {onToggle = ToggleCollapsable
+ ,isCollapsed = model.isCollapsed
+ ,label = Element.row Grid.compact
+ [ Element.html <|
+ if model.isCollapsed then
+ Heroicons.cheveronRight [ Attributes.width 20]
+ else
+ Heroicons.cheveronDown [ Attributes.width 20]
+ , Element.el Heading.h4 <|Element.text <| "Title"
+ ]
+ ,content = Element.text <| "Hello World"
+ }
+```
+
+-}
+collapsable :
+ { onToggle : Bool -> msg
+ , isCollapsed : Bool
+ , label : Element msg
+ , content : Element msg
+ }
+ -> Element msg
+collapsable { onToggle, isCollapsed, label, content } =
+ Element.column [] <|
+ [ Input.button []
+ { onPress = Just <| onToggle <| not isCollapsed
+ , label = label
+ }
+ ]
+ ++ (if isCollapsed then
+ []
+
+ else
+ [ content ]
+ )
+
+
+{-| A dialog element displaying important information.
+
+```
+ Framework.Layout
+ [ Element.inFront <|
+ if model.displayDialog then
+ Widget.dialog
+ { onDismiss = Just <| ToggleDialog False
+ , content =
+ [ "This is a dialog window"
+ |> Element.text
+ , Input.button []
+ {onPress = Just <| ToggleDialog False
+ , label = Element.text "Ok"
+ }
+ ]
+ |> Element.column []
+ }
+ else Element.none
+ ] <|
+ Element.text "some Content"
+```
+
+-}
+dialog :
+ { onDismiss : Maybe msg
+ , content : Element msg
+ }
+ -> Element msg
+dialog { onDismiss, content } =
+ content
+ |> Element.el
+ [ Element.centerX
+ , Element.centerY
+ ]
+ |> Element.el
+ ([ Element.width <| Element.fill
+ , Element.height <| Element.fill
+ , Background.color <| Element.rgba255 0 0 0 0.5
+ ]
+ ++ (onDismiss
+ |> Maybe.map (Events.onClick >> List.singleton)
+ |> Maybe.withDefault []
+ )
+ )
+
+
+{-| A Carousel circles through a non empty list of contents.
+
+```
+ Widget.carousel
+ {content = ("Blue",["Yellow", "Green" , "Red" ]|> Array.fromList)
+ ,current = model.carousel
+ , label = \c ->
+ [ Input.button [Element.centerY]
+ { onPress = Just <|
+ SetCarousel <|
+ (\x -> if x < 0 then 0 else x) <|
+ model.carousel - 1
+ , label = "<" |> Element.text
+ }
+ , c |> Element.text
+ , Input.button [Element.centerY]
+ { onPress = Just <|
+ SetCarousel <|
+ (\x -> if x > 3 then 3 else x) <|
+ model.carousel + 1
+ , label = ">" |> Element.text
+ }
+ ]
+ |> Element.row [Element.centerX, Element.width<| Element.shrink]
+ }
+```
+
+-}
+carousel :
+ { content : ( a, Array a )
+ , current : Int
+ , label : a -> Element msg
+ }
+ -> Element msg
+carousel { content, current, label } =
+ let
+ ( head, tail ) =
+ content
+ in
+ (if current <= 0 then
+ head
+
+ else if current > Array.length tail then
+ tail
+ |> Array.get (Array.length tail - 1)
+ |> Maybe.withDefault head
+
+ else
+ tail
+ |> Array.get (current - 1)
+ |> Maybe.withDefault head
+ )
+ |> label
diff --git a/src/Widget/FilterSelect.elm b/src/Widget/FilterSelect.elm
new file mode 100644
index 0000000..ea0a734
--- /dev/null
+++ b/src/Widget/FilterSelect.elm
@@ -0,0 +1,93 @@
+module Widget.FilterSelect exposing (Model, Msg(..), init, update, viewInput, viewOptions)
+
+{-|
+
+@docs Model, Msg, init, update, viewInput, viewOptions
+
+-}
+
+import Element exposing (Attribute, Element)
+import Element.Input as Input exposing (Placeholder)
+import Set exposing (Set)
+
+
+{-| The Model
+-}
+type alias Model =
+ { raw : String
+ , selected : Maybe String
+ , options : Set String
+ }
+
+
+{-| The Msg is exposed by design. You can unselect by sending `Selected Nothing`.
+-}
+type Msg
+ = ChangedRaw String
+ | Selected (Maybe String)
+
+
+{-| The initial state contains the set of possible options.
+-}
+init : Set String -> Model
+init options =
+ { raw = ""
+ , selected = Nothing
+ , options = options
+ }
+
+
+{-| Updates the Model
+-}
+update : Msg -> Model -> Model
+update msg model =
+ case msg of
+ ChangedRaw string ->
+ { model
+ | raw = string
+ }
+
+ Selected maybe ->
+ { model
+ | selected = maybe
+ }
+ |> (case maybe of
+ Just string ->
+ \m -> { m | raw = string }
+
+ Nothing ->
+ identity
+ )
+
+
+{-| A wrapper around Input.text.
+-}
+viewInput :
+ List (Attribute msg)
+ -> Model
+ ->
+ { msgMapper : Msg -> msg
+ , placeholder : Maybe (Placeholder msg)
+ , label : String
+ }
+ -> Element msg
+viewInput attributes model { msgMapper, placeholder, label } =
+ Input.text attributes
+ { onChange = ChangedRaw >> msgMapper
+ , text = model.raw
+ , placeholder = placeholder
+ , label = Input.labelHidden label
+ }
+
+
+{-| Returns a List of all options that matches the filter.
+-}
+viewOptions : Model -> List String
+viewOptions { raw, options } =
+ if raw == "" then
+ []
+
+ else
+ options
+ |> Set.filter (String.toUpper >> String.contains (raw |> String.toUpper))
+ |> Set.toList
diff --git a/src/Widget/ScrollingNav.elm b/src/Widget/ScrollingNav.elm
new file mode 100644
index 0000000..3286711
--- /dev/null
+++ b/src/Widget/ScrollingNav.elm
@@ -0,0 +1,195 @@
+module Widget.ScrollingNav exposing
+ ( Model, Msg, init, update, subscriptions, view, viewSections
+ , jumpTo, syncPositions
+ )
+
+{-| The Scrolling Nav is a navigation bar thats updates while you scroll through
+the page. Clicking on a navigation button will scroll directly to that section.
+
+
+# Basics
+
+@docs Model, Msg, init, update, subscriptions, view, viewSections
+
+
+# Operations
+
+@docs jumpTo, syncPositions
+
+-}
+
+import Browser.Dom as Dom
+import Element exposing (Element)
+import Framework.Grid as Grid
+import Html.Attributes as Attributes
+import IntDict exposing (IntDict)
+import Task
+import Time
+
+
+{-| -}
+type alias Model elem =
+ { labels : elem -> String
+ , positions : IntDict String
+ , arrangement : List elem
+ , scrollPos : Int
+ }
+
+
+{-| -}
+type Msg elem
+ = GotHeaderPos elem (Result Dom.Error Int)
+ | ChangedViewport (Result Dom.Error ())
+ | SyncPosition Int
+ | JumpTo elem
+ | TimePassed
+
+
+{-| -}
+init :
+ { labels : elem -> String
+ , arrangement : List elem
+ }
+ -> ( Model elem, Cmd (Msg elem) )
+init { labels, arrangement } =
+ { labels = labels
+ , positions = IntDict.empty
+ , arrangement = arrangement
+ , scrollPos = 0
+ }
+ |> (\a ->
+ ( a
+ , syncPositions a
+ )
+ )
+
+
+{-| -}
+update : Msg elem -> Model elem -> ( Model elem, Cmd (Msg elem) )
+update msg model =
+ case msg of
+ GotHeaderPos label result ->
+ ( case result of
+ Ok pos ->
+ { model
+ | positions =
+ model.positions
+ |> IntDict.insert pos
+ (label |> model.labels)
+ }
+
+ Err _ ->
+ model
+ , Cmd.none
+ )
+
+ ChangedViewport _ ->
+ ( model, Cmd.none )
+
+ SyncPosition pos ->
+ ( { model
+ | scrollPos = pos
+ }
+ , Cmd.none
+ )
+
+ TimePassed ->
+ ( model
+ , Dom.getViewport
+ |> Task.map (.viewport >> .y >> round)
+ |> Task.perform SyncPosition
+ )
+
+ JumpTo elem ->
+ ( model
+ , model
+ |> jumpTo elem
+ )
+
+
+{-| -}
+subscriptions : Sub (Msg msg)
+subscriptions =
+ Time.every 1000 (always TimePassed)
+
+
+{-| -}
+jumpTo : elem -> Model elem -> Cmd (Msg msg)
+jumpTo section { labels } =
+ Dom.getElement (section |> labels)
+ |> Task.andThen
+ (\{ element } ->
+ Dom.setViewport 0 element.y
+ )
+ |> Task.attempt ChangedViewport
+
+
+{-| -}
+syncPositions : Model elem -> Cmd (Msg elem)
+syncPositions { labels, arrangement } =
+ arrangement
+ |> List.map
+ (\label ->
+ Dom.getElement (labels label)
+ |> Task.map
+ (.element
+ >> .y
+ >> round
+ )
+ |> Task.attempt
+ (GotHeaderPos label)
+ )
+ |> Cmd.batch
+
+
+{-| -}
+viewSections :
+ { label : String -> Element msg
+ , fromString : String -> Maybe elem
+ , msgMapper : Msg elem -> msg
+ }
+ -> Model elem
+ ->
+ { selected : Maybe elem
+ , options : List elem
+ , label : elem -> Element msg
+ , onChange : elem -> msg
+ }
+viewSections { label, fromString, msgMapper } { arrangement, scrollPos, labels, positions } =
+ let
+ current =
+ positions
+ |> IntDict.before (scrollPos + 1)
+ |> Maybe.map Just
+ |> Maybe.withDefault (positions |> IntDict.after (scrollPos + 1))
+ |> Maybe.map Tuple.second
+ |> Maybe.andThen fromString
+ in
+ { selected = current
+ , options = arrangement
+ , label = \elem -> label (elem |> labels)
+ , onChange = JumpTo >> msgMapper
+ }
+
+
+{-| -}
+view :
+ (elem -> Element msg)
+ -> Model elem
+ -> Element msg
+view asElement { labels, arrangement } =
+ arrangement
+ |> List.map
+ (\header ->
+ Element.el
+ [ header
+ |> labels
+ |> Attributes.id
+ |> Element.htmlAttribute
+ , Element.width <| Element.fill
+ ]
+ <|
+ asElement <|
+ header
+ )
+ |> Element.column Grid.simple
diff --git a/src/Widget/Snackbar.elm b/src/Widget/Snackbar.elm
new file mode 100644
index 0000000..aa2eadc
--- /dev/null
+++ b/src/Widget/Snackbar.elm
@@ -0,0 +1,94 @@
+module Widget.Snackbar exposing
+ ( Model, init, current, timePassed
+ , insert, insertFor, dismiss
+ )
+
+{-| A [snackbar](https://material.io/components/snackbars/) shows notification, one at a time.
+
+
+# Basics
+
+@docs Model, init, current, timePassed
+
+
+# Operations
+
+@docs insert, insertFor, dismiss
+
+-}
+
+import Queue exposing (Queue)
+
+
+{-| A snackbar has a queue of Notifications, each with the amount of ms the message should be displayed
+-}
+type alias Model a =
+ { queue : Queue ( a, Int )
+ , current : Maybe ( a, Int )
+ }
+
+
+{-| Inital state
+-}
+init : Model a
+init =
+ { queue = Queue.empty
+ , current = Nothing
+ }
+
+
+{-| Insert a message that will last for 10 seconds.
+-}
+insert : a -> Model a -> Model a
+insert =
+ insertFor 10000
+
+
+{-| Insert a message for a specific amount of milli seconds.
+-}
+insertFor : Int -> a -> Model a -> Model a
+insertFor removeIn a model =
+ case model.current of
+ Nothing ->
+ { model | current = Just ( a, removeIn ) }
+
+ Just _ ->
+ { model | queue = model.queue |> Queue.enqueue ( a, removeIn ) }
+
+
+{-| Dismiss the current message.
+-}
+dismiss : Model a -> Model a
+dismiss model =
+ { model | current = Nothing }
+
+
+{-| Updates the model. This functions should be called regularly.
+The first argument is the milli seconds that passed since the last time the function was called.
+-}
+timePassed : Int -> Model a -> Model a
+timePassed ms model =
+ case model.current of
+ Nothing ->
+ let
+ ( c, queue ) =
+ model.queue |> Queue.dequeue
+ in
+ { model
+ | current = c
+ , queue = queue
+ }
+
+ Just ( _, removeIn ) ->
+ if removeIn <= ms then
+ model |> dismiss
+
+ else
+ { model | current = model.current |> Maybe.map (Tuple.mapSecond ((+) -ms)) }
+
+
+{-| Returns the current element.
+-}
+current : Model a -> Maybe a
+current model =
+ model.current |> Maybe.map Tuple.first
diff --git a/src/Widget/SortTable.elm b/src/Widget/SortTable.elm
new file mode 100644
index 0000000..719d157
--- /dev/null
+++ b/src/Widget/SortTable.elm
@@ -0,0 +1,219 @@
+module Widget.SortTable exposing
+ ( Model, init, view, sortBy
+ , intColumn, floatColumn, stringColumn
+ )
+
+{-| A Sortable list allows you to sort coulmn.
+
+```
+ SortTable.view
+ { content =
+ [ {id = 1, name = "Antonio", rating = 2.456}
+ , {id = 2, name = "Ana", rating = 1.34}
+ , {id = 3, name = "Alfred", rating = 4.22}
+ , {id = 4, name = "Thomas", rating = 3 }
+ ]
+ , columns =
+ [ SortTable.intColumn
+ { title = "Id"
+ , value = .id
+ , toString = \int -> "#" ++ String.fromInt int
+ }
+ , SortTable.stringColumn
+ { title = "Name"
+ , value = .name
+ , toString = identity
+ }
+ , SortTable.floatColumn
+ { title = "rating"
+ , value = .rating
+ , toString = String.fromFloat
+ }
+ ]
+ , model = model
+ }
+ |> (\{data,columns} ->
+ {data = data
+ ,columns = columns
+ |> List.map (\config->
+ { header =
+ Input.button [Font.bold]
+ { onPress =
+ { title = config.header
+ , asc =
+ if config.header == model.title then
+ not model.asc
+ else
+ True
+ }
+ |> SortBy
+ |> Just
+ , label =
+ if config.header == model.title then
+ [ config.header |> Element.text
+ , Element.text <|
+ if model.asc then
+ "/\"
+ else
+ "\/"
+ ]
+ |> Element.row [Font.bold]
+ else
+ config.header |> Element.text
+ }
+ , view = config.view >> Element.text
+ , width = Element.fill
+ }
+ )
+ })
+ |> Element.table []
+```
+
+
+# Basics
+
+@docs Model, init, view, sortBy
+
+
+# Columns
+
+@docs intColumn, floatColumn, stringColumn
+
+-}
+
+
+type ColumnType a
+ = StringColumn { value : a -> String, toString : String -> String }
+ | IntColumn { value : a -> Int, toString : Int -> String }
+ | FloatColumn { value : a -> Float, toString : Float -> String }
+
+
+{-| The Model contains the sorting column name and if ascending or descending.
+-}
+type alias Model =
+ { title : String
+ , asc : Bool
+ }
+
+
+type alias Column a =
+ { title : String
+ , content : ColumnType a
+ }
+
+
+{-| The initial State setting the sorting column name to the empty string.
+-}
+init : Model
+init =
+ { title = "", asc = True }
+
+
+{-| A Column containing a Int
+-}
+intColumn : { title : String, value : a -> Int, toString : Int -> String } -> Column a
+intColumn { title, value, toString } =
+ { title = title
+ , content = IntColumn { value = value, toString = toString }
+ }
+
+
+{-| A Column containing a Float
+-}
+floatColumn : { title : String, value : a -> Float, toString : Float -> String } -> Column a
+floatColumn { title, value, toString } =
+ { title = title
+ , content = FloatColumn { value = value, toString = toString }
+ }
+
+
+{-| A Column containing a String
+-}
+stringColumn : { title : String, value : a -> String, toString : String -> String } -> Column a
+stringColumn { title, value, toString } =
+ { title = title
+ , content = StringColumn { value = value, toString = toString }
+ }
+
+
+{-| Change the sorting criteras.
+
+```
+ sortBy =
+ identity
+```
+
+-}
+sortBy : { title : String, asc : Bool } -> Model
+sortBy =
+ identity
+
+
+{-| The View
+-}
+view :
+ { content : List a
+ , columns : List (Column a)
+ , model : Model
+ }
+ ->
+ { data : List a
+ , columns : List { header : String, view : a -> String }
+ }
+view { content, columns, model } =
+ let
+ findTitle : List (Column a) -> Maybe (ColumnType a)
+ findTitle list =
+ case list of
+ [] ->
+ Nothing
+
+ head :: tail ->
+ if head.title == model.title then
+ Just head.content
+
+ else
+ findTitle tail
+ in
+ { data =
+ content
+ |> (columns
+ |> findTitle
+ |> Maybe.map
+ (\c ->
+ case c of
+ StringColumn { value } ->
+ List.sortBy value
+
+ IntColumn { value } ->
+ List.sortBy value
+
+ FloatColumn { value } ->
+ List.sortBy value
+ )
+ |> Maybe.withDefault identity
+ )
+ |> (if model.asc then
+ identity
+
+ else
+ List.reverse
+ )
+ , columns =
+ columns
+ |> List.map
+ (\column ->
+ { header = column.title
+ , view =
+ case column.content of
+ IntColumn { value, toString } ->
+ value >> toString
+
+ FloatColumn { value, toString } ->
+ value >> toString
+
+ StringColumn { value, toString } ->
+ value >> toString
+ }
+ )
+ }
diff --git a/src/Widget/ValidatedInput.elm b/src/Widget/ValidatedInput.elm
new file mode 100644
index 0000000..980c579
--- /dev/null
+++ b/src/Widget/ValidatedInput.elm
@@ -0,0 +1,144 @@
+module Widget.ValidatedInput exposing
+ ( Model, Msg, init, update, view
+ , getError, getRaw, getValue
+ )
+
+{-| The validated Input is a wrapper around `Input.text`.
+They can validate the input and return an error if nessarry.
+
+
+# Basics
+
+@docs Model, Msg, init, update, view
+
+
+# Access the Model
+
+@docs getError, getRaw, getValue
+
+-}
+
+import Element exposing (Attribute, Element)
+import Element.Events as Events
+import Element.Input as Input exposing (Placeholder)
+
+
+{-| -}
+type Model err a
+ = Model
+ { raw : Maybe String
+ , value : a
+ , err : Maybe err
+ , validator : String -> Result err a
+ , toString : a -> String
+ }
+
+
+{-| -}
+getRaw : Model err a -> String
+getRaw (Model { raw, value, toString }) =
+ case raw of
+ Just string ->
+ string
+
+ Nothing ->
+ value |> toString
+
+
+{-| -}
+getValue : Model err a -> a
+getValue (Model { value }) =
+ value
+
+
+{-| -}
+getError : Model err a -> Maybe err
+getError (Model { err }) =
+ err
+
+
+{-| -}
+type Msg
+ = ChangedRaw String
+ | LostFocus
+ | StartEditing
+
+
+{-| -}
+init : { value : a, validator : String -> Result err a, toString : a -> String } -> Model err a
+init { validator, toString, value } =
+ Model
+ { raw = Nothing
+ , value = value
+ , err = Nothing
+ , validator = validator
+ , toString = toString
+ }
+
+
+{-| -}
+update : Msg -> Model err a -> Model err a
+update msg (Model model) =
+ case msg of
+ StartEditing ->
+ Model
+ { model
+ | raw = model.value |> model.toString |> Just
+ }
+
+ ChangedRaw string ->
+ Model
+ { model
+ | raw = Just string
+ , err = Nothing
+ }
+
+ LostFocus ->
+ case model.raw of
+ Just string ->
+ case model.validator string of
+ Ok value ->
+ Model
+ { model
+ | value = value
+ , raw = Nothing
+ , err = Nothing
+ }
+
+ Err err ->
+ Model
+ { model
+ | raw = Nothing
+ , err = Just err
+ }
+
+ Nothing ->
+ Model model
+
+
+{-| -}
+view :
+ List (Attribute msg)
+ -> Model err a
+ ->
+ { msgMapper : Msg -> msg
+ , placeholder : Maybe (Placeholder msg)
+ , label : String
+ , readOnly : a -> Element msg
+ }
+ -> Element msg
+view attributes (Model model) { msgMapper, placeholder, label, readOnly } =
+ case model.raw of
+ Just string ->
+ Input.text (attributes ++ [ Events.onLoseFocus <| msgMapper <| LostFocus ])
+ { onChange = ChangedRaw >> msgMapper
+ , text = string
+ , placeholder = placeholder
+ , label = Input.labelHidden label
+ }
+
+ Nothing ->
+ Input.button []
+ { onPress = Just (StartEditing |> msgMapper)
+ , label = model.value |> readOnly
+ }