516 lines
12 KiB
Elm
516 lines
12 KiB
Elm
module Layout exposing
|
|
( twoBlocks
|
|
, tab, sideIconBar
|
|
, iconAsElement, iconAsIcon
|
|
, containedButton, outlinedButton, textButton
|
|
, textInput, passwordInput
|
|
, header, stdText
|
|
, itemWithSubtext
|
|
, sideList, radioButtons
|
|
, loadingIndicator, svg
|
|
)
|
|
|
|
{-|
|
|
|
|
|
|
# Layout
|
|
|
|
The layout module exposes some boilerplate functions that have produce a
|
|
beautiful Material design Elm webpage.
|
|
|
|
|
|
## Screen layout
|
|
|
|
@docs twoBlocks
|
|
|
|
|
|
## Elements
|
|
|
|
@docs tab, sideIconBar
|
|
|
|
|
|
## Icons
|
|
|
|
@docs iconAsElement, iconAsIcon
|
|
|
|
|
|
## Buttons
|
|
|
|
@docs containedButton, outlinedButton, textButton
|
|
|
|
|
|
## Text fields
|
|
|
|
@docs textInput, passwordInput
|
|
|
|
|
|
## Text
|
|
|
|
@docs header, stdText
|
|
|
|
|
|
## Items in a list
|
|
|
|
@docs itemWithSubtext
|
|
|
|
|
|
## Lists
|
|
|
|
@docs sideList, radioButtons
|
|
|
|
|
|
## Other elements
|
|
|
|
@docs loadingIndicator, svg
|
|
|
|
-}
|
|
|
|
import Color exposing (Color)
|
|
import Element exposing (Element)
|
|
import Element.Background
|
|
import Element.Events
|
|
import Element.Font
|
|
import Element.Input
|
|
import Html.Attributes
|
|
import Material.Icons.Types
|
|
import Svg exposing (Svg)
|
|
import Svg.Attributes
|
|
import Theme
|
|
import Widget
|
|
import Widget.Customize as Customize
|
|
import Widget.Icon exposing (Icon)
|
|
import Widget.Material as Material
|
|
import Widget.Material.Typography
|
|
|
|
|
|
{-| A contained button representing the most important action of a group.
|
|
-}
|
|
containedButton :
|
|
{ buttonColor : Color
|
|
, clickColor : Color
|
|
, icon : Icon msg
|
|
, onPress : Maybe msg
|
|
, text : String
|
|
}
|
|
-> Element msg
|
|
containedButton data =
|
|
Widget.button
|
|
({ primary = data.buttonColor, onPrimary = data.clickColor }
|
|
|> singlePalette
|
|
|> Material.containedButton
|
|
|> Customize.elementButton [ Element.width Element.fill ]
|
|
)
|
|
{ text = data.text, icon = data.icon, onPress = data.onPress }
|
|
|
|
|
|
header : String -> Element msg
|
|
header =
|
|
Element.text
|
|
>> Element.el Widget.Material.Typography.h1
|
|
>> List.singleton
|
|
>> Element.paragraph []
|
|
|
|
|
|
iconAsElement :
|
|
{ color : Color
|
|
, height : Int
|
|
, icon : Material.Icons.Types.Icon msg
|
|
, width : Int
|
|
}
|
|
-> Element msg
|
|
iconAsElement data =
|
|
data.icon
|
|
|> iconAsIcon
|
|
|> (|>) { size = Basics.min data.height data.width, color = data.color }
|
|
|> Element.el [ Element.centerX, Element.centerY ]
|
|
|> Element.el
|
|
[ Element.height (Element.px data.height)
|
|
, Element.width (Element.px data.width)
|
|
]
|
|
|
|
|
|
iconAsIcon : Material.Icons.Types.Icon msg -> Widget.Icon.Icon msg
|
|
iconAsIcon =
|
|
Widget.Icon.elmMaterialIcons Material.Icons.Types.Color
|
|
|
|
|
|
{-| Multiline item
|
|
-}
|
|
itemWithSubtext :
|
|
{ color : Color
|
|
, leftIcon : Widget.Icon.Icon msg
|
|
, onPress : Maybe msg
|
|
, rightIcon : Widget.Icon.Icon msg
|
|
, text : String
|
|
, title : String
|
|
}
|
|
-> Widget.Item msg
|
|
itemWithSubtext data =
|
|
Widget.multiLineItem
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.multiLineItem
|
|
)
|
|
{ content = data.rightIcon
|
|
, icon = data.leftIcon
|
|
, onPress = data.onPress
|
|
, title = data.title
|
|
, text = data.text
|
|
}
|
|
|
|
|
|
{-| Circular loading bar indicator
|
|
-}
|
|
loadingIndicator :
|
|
{ color : Color
|
|
}
|
|
-> Element msg
|
|
loadingIndicator data =
|
|
Widget.circularProgressIndicator
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.progressIndicator
|
|
)
|
|
Nothing
|
|
|
|
|
|
{-| An outlined button representing an important action within a group.
|
|
-}
|
|
outlinedButton :
|
|
{ color : Color
|
|
, icon : Icon msg
|
|
, onPress : Maybe msg
|
|
, text : String
|
|
}
|
|
-> Element msg
|
|
outlinedButton data =
|
|
Widget.button
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.outlinedButton
|
|
)
|
|
{ text = data.text, icon = data.icon, onPress = data.onPress }
|
|
|
|
|
|
{-| Show a password field
|
|
-}
|
|
passwordInput :
|
|
{ color : Color
|
|
, label : String
|
|
, onChange : String -> msg
|
|
, placeholder : Maybe String
|
|
, show : Bool
|
|
, text : String
|
|
}
|
|
-> Element msg
|
|
passwordInput data =
|
|
Widget.currentPasswordInputV2
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.passwordInput
|
|
|> Customize.elementRow [ Element.width Element.fill ]
|
|
)
|
|
{ label = data.label
|
|
, onChange = data.onChange
|
|
, placeholder =
|
|
data.placeholder
|
|
|> Maybe.map Element.text
|
|
|> Maybe.map (Element.Input.placeholder [])
|
|
, show = data.show
|
|
, text = data.text
|
|
}
|
|
|
|
|
|
{-| Redio buttons are side-by-side buttons that only allowed up to one to be
|
|
selected.
|
|
-}
|
|
radioButtons :
|
|
{ color : Color
|
|
, items : List ( Bool, a )
|
|
, toIcon : a -> Icon msg
|
|
, toString : a -> String
|
|
, onChange : a -> msg
|
|
}
|
|
-> Element msg
|
|
radioButtons data =
|
|
data.items
|
|
|> List.map
|
|
(Tuple.mapSecond
|
|
(\item ->
|
|
{ text = data.toString item
|
|
, icon =
|
|
\{ size, color } ->
|
|
Element.text (data.toString item)
|
|
, onPress = Just (data.onChange item)
|
|
}
|
|
)
|
|
)
|
|
|> Widget.toggleRow
|
|
{ elementRow = Material.toggleRow
|
|
, content =
|
|
{ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.toggleButton
|
|
}
|
|
|
|
|
|
{-| Create a simple palette.
|
|
-}
|
|
singlePalette : { primary : Color, onPrimary : Color } -> Material.Palette
|
|
singlePalette { primary, onPrimary } =
|
|
{ primary = primary
|
|
, secondary = primary
|
|
, background = primary
|
|
, surface = primary
|
|
, error = primary
|
|
, on =
|
|
{ primary = onPrimary
|
|
, secondary = onPrimary
|
|
, background = onPrimary
|
|
, surface = onPrimary
|
|
, error = onPrimary
|
|
}
|
|
}
|
|
|
|
|
|
sideIconBar :
|
|
{ colorBackground : Color
|
|
, colorText : Color
|
|
, height : Int
|
|
, items : List { icon : Widget.Icon.Icon msg, onPress : msg, text : String }
|
|
, width : Int
|
|
}
|
|
-> Element msg
|
|
sideIconBar data =
|
|
let
|
|
buttonHeight =
|
|
round (toFloat data.width * 1.618)
|
|
|
|
fontSize =
|
|
data.width // 6
|
|
|
|
iconSize =
|
|
data.width * 3 // 5
|
|
in
|
|
data.items
|
|
|> List.map
|
|
(\item ->
|
|
[ item.icon { size = iconSize, color = data.colorText }
|
|
|> Element.el [ Element.centerX ]
|
|
, Element.paragraph [] [ Element.text item.text ]
|
|
]
|
|
|> Element.column
|
|
[ Element.centerX
|
|
, Element.centerY
|
|
, Element.Font.bold
|
|
, Element.Font.center
|
|
, Element.Font.size fontSize
|
|
, Element.htmlAttribute (Html.Attributes.style "cursor" "pointer")
|
|
]
|
|
|> Element.el
|
|
[ Element.centerY
|
|
, Element.Events.onClick item.onPress
|
|
, Element.height (Element.px data.width)
|
|
, Element.width (Element.px data.width)
|
|
]
|
|
|> Element.el
|
|
[ Element.height (Element.px buttonHeight)
|
|
]
|
|
)
|
|
|> Element.column
|
|
[ Element.Background.color (Theme.toElmUiColor data.colorBackground)
|
|
, Element.height (Element.px data.height)
|
|
, Element.scrollbarY
|
|
, Element.width (Element.px data.width)
|
|
]
|
|
|
|
|
|
sideList : { color : Color, items : List (Widget.Item msg), width : Int } -> Element msg
|
|
sideList data =
|
|
let
|
|
width px =
|
|
Element.width (Element.px px)
|
|
in
|
|
Widget.itemList
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.sideSheet
|
|
)
|
|
data.items
|
|
|> Element.el [ Element.centerX, width (Basics.min 360 data.width) ]
|
|
|> Element.el [ width data.width ]
|
|
|
|
|
|
stdText : String -> Element msg
|
|
stdText =
|
|
Element.text >> List.singleton >> Element.paragraph []
|
|
|
|
|
|
svg :
|
|
{ aspectRatio : Float
|
|
, height : Int
|
|
, svg : Svg msg
|
|
, width : Int
|
|
, viewMinX : Float
|
|
, viewMaxX : Float
|
|
, viewMinY : Float
|
|
, viewMaxY : Float
|
|
}
|
|
-> Element msg
|
|
svg data =
|
|
let
|
|
givenWidth =
|
|
toFloat data.width
|
|
|
|
givenHeight =
|
|
toFloat data.height
|
|
|
|
scaleFactorWidth =
|
|
givenHeight / givenWidth
|
|
|
|
innerWidth =
|
|
if scaleFactorWidth > data.aspectRatio then
|
|
givenWidth
|
|
|
|
else
|
|
givenHeight / data.aspectRatio
|
|
|
|
innerHeight =
|
|
if scaleFactorWidth > data.aspectRatio then
|
|
givenWidth * data.aspectRatio
|
|
|
|
else
|
|
givenHeight
|
|
in
|
|
Svg.svg
|
|
[ [ data.viewMinX, data.viewMinY, data.viewMaxX - data.viewMinX, data.viewMaxY - data.viewMinY ]
|
|
|> List.map String.fromFloat
|
|
|> String.join " "
|
|
|> Svg.Attributes.viewBox
|
|
, Svg.Attributes.width (String.fromFloat innerWidth)
|
|
, Svg.Attributes.height (String.fromFloat innerHeight)
|
|
]
|
|
[ data.svg ]
|
|
|> Element.html
|
|
|> Element.el [ Element.centerX, Element.centerY ]
|
|
|> Element.el
|
|
[ Element.height (Element.px data.height)
|
|
, Element.width (Element.px data.width)
|
|
]
|
|
|
|
|
|
{-| A tab selector that always has an item selected.
|
|
-}
|
|
tab :
|
|
{ color : Color
|
|
, content : Int -> Element msg
|
|
, items : List { text : String, icon : Icon msg }
|
|
, onSelect : Int -> msg
|
|
, selected : Int
|
|
}
|
|
-> Element msg
|
|
tab data =
|
|
Widget.tab
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.tab
|
|
)
|
|
{ tabs =
|
|
{ onSelect = data.onSelect >> Just
|
|
, options = data.items
|
|
, selected = Just data.selected
|
|
}
|
|
, content = \_ -> data.content data.selected
|
|
}
|
|
|
|
|
|
{-| A text button representing an important action within a group.
|
|
-}
|
|
textButton :
|
|
{ icon : Icon msg
|
|
, onPress : Maybe msg
|
|
, text : String
|
|
, color : Color
|
|
}
|
|
-> Element msg
|
|
textButton data =
|
|
Widget.button
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.textButton
|
|
)
|
|
{ text = data.text, icon = data.icon, onPress = data.onPress }
|
|
|
|
|
|
{-| Text input element.
|
|
-}
|
|
textInput :
|
|
{ color : Color
|
|
, label : String
|
|
, onChange : String -> msg
|
|
, placeholder : Maybe String
|
|
, text : String
|
|
}
|
|
-> Element msg
|
|
textInput data =
|
|
Widget.textInput
|
|
({ primary = data.color, onPrimary = data.color }
|
|
|> singlePalette
|
|
|> Material.textInput
|
|
|> Customize.elementRow [ Element.width Element.fill ]
|
|
)
|
|
{ chips = []
|
|
, text = data.text
|
|
, placeholder =
|
|
data.placeholder
|
|
|> Maybe.map Element.text
|
|
|> Maybe.map (Element.Input.placeholder [])
|
|
, label = data.label
|
|
, onChange = data.onChange
|
|
}
|
|
|
|
|
|
{-| Two blocks either next to each other or below each other, depending on the
|
|
screen shape.
|
|
-}
|
|
twoBlocks :
|
|
{ height : Int
|
|
, el1 : { height : Int, width : Int } -> Element msg
|
|
, el2 : { height : Int, width : Int } -> Element msg
|
|
, width : Int
|
|
}
|
|
-> Element msg
|
|
twoBlocks data =
|
|
let
|
|
goesVertical =
|
|
2 * data.width <= 3 * data.height
|
|
|
|
direction =
|
|
if goesVertical then
|
|
Element.column
|
|
|
|
else
|
|
Element.row
|
|
|
|
width =
|
|
if goesVertical then
|
|
data.width
|
|
|
|
else
|
|
data.width // 2
|
|
|
|
height =
|
|
if goesVertical then
|
|
data.height // 2
|
|
|
|
else
|
|
data.height
|
|
in
|
|
direction
|
|
[ Element.height (Element.px data.height)
|
|
, Element.width (Element.px data.width)
|
|
]
|
|
[ data.el1 { height = height, width = width }
|
|
, data.el2 { height = height, width = width }
|
|
]
|