mirror of
https://github.com/Orasund/elm-ui-widgets.git
synced 2024-11-26 07:34:39 +03:00
Added Dialog, and more...
This commit is contained in:
parent
82b59530fd
commit
b73a19d3ce
1
elm.json
1
elm.json
@ -18,6 +18,7 @@
|
|||||||
"elm/browser": "1.0.2 <= v < 2.0.0",
|
"elm/browser": "1.0.2 <= v < 2.0.0",
|
||||||
"elm/core": "1.0.0 <= v < 2.0.0",
|
"elm/core": "1.0.0 <= v < 2.0.0",
|
||||||
"elm/html": "1.0.0 <= v < 2.0.0",
|
"elm/html": "1.0.0 <= v < 2.0.0",
|
||||||
|
"elm/svg": "1.0.1 <= v < 2.0.0",
|
||||||
"elm/time": "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",
|
"elm-community/intdict": "3.0.0 <= v < 4.0.0",
|
||||||
"jasonliang512/elm-heroicons": "1.0.2 <= v < 2.0.0",
|
"jasonliang512/elm-heroicons": "1.0.2 <= v < 2.0.0",
|
||||||
|
@ -91,10 +91,9 @@ update msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
filterSelect : FilterSelect.Model -> Element Msg
|
filterSelect : FilterSelect.Model -> (String,Element Msg)
|
||||||
filterSelect model =
|
filterSelect model =
|
||||||
Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) <|
|
( "Filter Select"
|
||||||
[ Element.el Heading.h3 <| Element.text "Filter Select"
|
|
||||||
, case model.selected of
|
, case model.selected of
|
||||||
Just selected ->
|
Just selected ->
|
||||||
Element.row Grid.compact
|
Element.row Grid.compact
|
||||||
@ -128,13 +127,12 @@ filterSelect model =
|
|||||||
)
|
)
|
||||||
|> Element.wrappedRow [ Element.spacing 10 ]
|
|> Element.wrappedRow [ Element.spacing 10 ]
|
||||||
]
|
]
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
validatedInput : ValidatedInput.Model () ( String, String ) -> Element Msg
|
validatedInput : ValidatedInput.Model () ( String, String ) -> (String,Element Msg)
|
||||||
validatedInput model =
|
validatedInput model =
|
||||||
Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) <|
|
( "Validated Input"
|
||||||
[ Element.el Heading.h3 <| Element.text "Validated Input"
|
|
||||||
, ValidatedInput.view Input.simple
|
, ValidatedInput.view Input.simple
|
||||||
model
|
model
|
||||||
{ label = "First Name, Sir Name"
|
{ label = "First Name, Sir Name"
|
||||||
@ -152,19 +150,19 @@ validatedInput model =
|
|||||||
|> Element.el (Tag.simple ++ Group.right ++ Color.primary)
|
|> Element.el (Tag.simple ++ Group.right ++ Color.primary)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
)
|
||||||
|
|
||||||
view : Model -> Element Msg
|
view : (Msg -> msg) -> Model ->
|
||||||
view model =
|
{ title : String
|
||||||
Element.column (Grid.section ++ [ Element.centerX ])
|
, description : String
|
||||||
[ Element.el Heading.h2 <| Element.text "Components"
|
, items : List (String,Element msg)
|
||||||
, "Components have a Model, an Update- and sometimes even a Subscription-function. It takes some time to set them up correctly."
|
}
|
||||||
|> Element.text
|
view msgMapper model =
|
||||||
|> List.singleton
|
{ title = "Components"
|
||||||
|> Element.paragraph []
|
, description = "Components have a Model, an Update- and sometimes even a Subscription-function. It takes some time to set them up correctly."
|
||||||
, Element.wrappedRow (Grid.simple ++ [Element.height <| Element.shrink]) <|
|
, items =
|
||||||
[ filterSelect model.filterSelect
|
[ filterSelect model.filterSelect
|
||||||
, validatedInput model.validatedInput
|
, validatedInput model.validatedInput
|
||||||
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
|> List.map (Tuple.mapSecond (Element.map msgMapper) )
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import Browser.Dom as Dom exposing (Viewport)
|
|||||||
import Browser.Events as Events
|
import Browser.Events as Events
|
||||||
import Browser.Navigation as Navigation
|
import Browser.Navigation as Navigation
|
||||||
import Component
|
import Component
|
||||||
import Element exposing (DeviceClass(..), Element)
|
import Element exposing (DeviceClass(..), Element,Attribute)
|
||||||
import Element.Input as Input
|
import Element.Input as Input
|
||||||
import Element.Font as Font
|
import Element.Font as Font
|
||||||
import Element.Border as Border
|
import Element.Border as Border
|
||||||
@ -29,21 +29,27 @@ import Stateless
|
|||||||
import Task
|
import Task
|
||||||
import Time
|
import Time
|
||||||
import Widget
|
import Widget
|
||||||
|
import Widget.Button exposing (ButtonStyle)
|
||||||
import Widget.FilterSelect as FilterSelect
|
import Widget.FilterSelect as FilterSelect
|
||||||
import Widget.ScrollingNav as ScrollingNav
|
import Widget.ScrollingNav as ScrollingNav
|
||||||
import Widget.Snackbar as Snackbar
|
import Widget.Snackbar as Snackbar
|
||||||
import Widget.ValidatedInput as ValidatedInput
|
import Widget.ValidatedInput as ValidatedInput
|
||||||
import Data.Section as Section exposing (Section(..))
|
import Data.Section as Section exposing (Section(..))
|
||||||
|
import Array
|
||||||
|
|
||||||
type alias LoadedModel =
|
type alias LoadedModel =
|
||||||
{ component : Component.Model
|
{ component : Component.Model
|
||||||
, stateless : Stateless.Model
|
, stateless : Stateless.Model
|
||||||
, reusable : Reusable.Model
|
, reusable : Reusable.Model
|
||||||
, scrollingNav : ScrollingNav.Model Section
|
, scrollingNav : ScrollingNav.Model Section
|
||||||
, layout : Layout
|
, layout : Layout LoadedMsg
|
||||||
, displayDialog : Bool
|
, displayDialog : Bool
|
||||||
, deviceClass : DeviceClass
|
, window : { height : Int, width : Int }
|
||||||
, search : String
|
, search :
|
||||||
|
{ raw : String
|
||||||
|
, current : String
|
||||||
|
, remaining : Int
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -58,7 +64,7 @@ type LoadedMsg
|
|||||||
| ComponentSpecific Component.Msg
|
| ComponentSpecific Component.Msg
|
||||||
| UpdateScrollingNav (ScrollingNav.Model Section -> ScrollingNav.Model Section)
|
| UpdateScrollingNav (ScrollingNav.Model Section -> ScrollingNav.Model Section)
|
||||||
| TimePassed Int
|
| TimePassed Int
|
||||||
| AddSnackbar String
|
| AddSnackbar (String,Bool)
|
||||||
| ToggleDialog Bool
|
| ToggleDialog Bool
|
||||||
| ChangedSidebar (Maybe Part)
|
| ChangedSidebar (Maybe Part)
|
||||||
| Resized { width : Int, height : Int }
|
| Resized { width : Int, height : Int }
|
||||||
@ -72,9 +78,64 @@ type Msg
|
|||||||
= LoadedSpecific LoadedMsg
|
= LoadedSpecific LoadedMsg
|
||||||
| GotViewport Viewport
|
| GotViewport Viewport
|
||||||
|
|
||||||
style : Style msg
|
textButton : ButtonStyle msg
|
||||||
|
textButton =
|
||||||
|
{ container = Button.simple
|
||||||
|
, label = Grid.simple
|
||||||
|
, disabled = Color.disabled
|
||||||
|
, active = Color.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleButton : ButtonStyle msg
|
||||||
|
simpleButton =
|
||||||
|
{ container = Button.simple ++ Color.primary
|
||||||
|
, label = Grid.simple
|
||||||
|
, disabled = Color.disabled
|
||||||
|
, active = Color.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
style : Style
|
||||||
|
{ dialog :
|
||||||
|
{ containerColumn : List (Attribute msg)
|
||||||
|
, title : List (Attribute msg)
|
||||||
|
, buttonRow : List (Attribute msg)
|
||||||
|
, accept : ButtonStyle msg
|
||||||
|
, dismiss : ButtonStyle msg
|
||||||
|
}
|
||||||
|
} msg
|
||||||
style =
|
style =
|
||||||
{ snackbar = Card.simple ++ Color.dark
|
{ dialog =
|
||||||
|
{ containerColumn =
|
||||||
|
Card.simple
|
||||||
|
++ Grid.simple
|
||||||
|
++ [ Element.width <| Element.minimum 280 <| Element.maximum 560 <| Element.fill ]
|
||||||
|
, title = Heading.h3
|
||||||
|
, buttonRow =
|
||||||
|
Grid.simple ++
|
||||||
|
[ Element.paddingEach
|
||||||
|
{ top = 28
|
||||||
|
, bottom = 0
|
||||||
|
, left = 0
|
||||||
|
, right = 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
, accept = simpleButton
|
||||||
|
, dismiss = textButton
|
||||||
|
}
|
||||||
|
, snackbar =
|
||||||
|
{ row =
|
||||||
|
Card.simple
|
||||||
|
++ Color.dark
|
||||||
|
++ Grid.simple
|
||||||
|
++ [ Element.paddingXY 8 6]
|
||||||
|
, button =
|
||||||
|
{ label = Grid.simple
|
||||||
|
, container = Button.simple ++ Color.dark
|
||||||
|
, disabled = Color.disabled
|
||||||
|
, active = Color.primary
|
||||||
|
}
|
||||||
|
, text = [Element.paddingXY 8 0]
|
||||||
|
}
|
||||||
, layout = Framework.responsiveLayout
|
, layout = Framework.responsiveLayout
|
||||||
, header =
|
, header =
|
||||||
Framework.container
|
Framework.container
|
||||||
@ -83,28 +144,42 @@ style =
|
|||||||
, Element.height <| Element.px <| 42
|
, Element.height <| Element.px <| 42
|
||||||
]
|
]
|
||||||
, menuButton =
|
, menuButton =
|
||||||
Button.simple ++ Group.center ++ Color.dark
|
{ label = Grid.simple
|
||||||
, menuButtonSelected =
|
, container = Button.simple ++ Group.center ++ Color.dark
|
||||||
Color.primary
|
, disabled = Color.disabled
|
||||||
|
, active = Color.primary
|
||||||
|
}
|
||||||
, sheetButton =
|
, sheetButton =
|
||||||
Button.fill
|
{ container =
|
||||||
++ Group.center
|
Button.fill
|
||||||
++ Color.light
|
++ Group.center
|
||||||
++ [Font.alignLeft]
|
++ Color.light
|
||||||
, sheetButtonSelected =
|
++ [Font.alignLeft]
|
||||||
Color.primary
|
, label = Grid.simple
|
||||||
, tabButton =
|
, disabled = Color.disabled
|
||||||
[ Element.height <| Element.px <| 42
|
, active = Color.primary
|
||||||
, Border.widthEach
|
}
|
||||||
{ top = 0,
|
, menuTabButton =
|
||||||
left = 0,
|
{ container =
|
||||||
right = 0,
|
[ Element.height <| Element.px <| 42
|
||||||
bottom = 8
|
, Border.widthEach
|
||||||
}
|
{ top = 0,
|
||||||
]
|
left = 0,
|
||||||
, tabButtonSelected =
|
right = 0,
|
||||||
[ Border.color Color.turquoise
|
bottom = 4
|
||||||
]
|
}
|
||||||
|
, Element.paddingEach
|
||||||
|
{ top = 12
|
||||||
|
, left = 8
|
||||||
|
, right = 8
|
||||||
|
, bottom = 4
|
||||||
|
}
|
||||||
|
, Border.color Color.black
|
||||||
|
]
|
||||||
|
, label = Grid.simple
|
||||||
|
, disabled = Color.disabled
|
||||||
|
, active = [ Border.color Color.turquoise ]
|
||||||
|
}
|
||||||
, sheet =
|
, sheet =
|
||||||
Color.light ++ [ Element.width <| Element.maximum 256 <| Element.fill]
|
Color.light ++ [ Element.width <| Element.maximum 256 <| Element.fill]
|
||||||
, menuIcon =
|
, menuIcon =
|
||||||
@ -116,7 +191,16 @@ style =
|
|||||||
, searchIcon =
|
, searchIcon =
|
||||||
Icons.search |> Element.html |> Element.el []
|
Icons.search |> Element.html |> Element.el []
|
||||||
, search =
|
, search =
|
||||||
Color.simple ++ [Font.color <| Element.rgb255 0 0 0 ]
|
Color.simple ++
|
||||||
|
Card.large ++
|
||||||
|
[Font.color <| Element.rgb255 0 0 0
|
||||||
|
, Element.padding 6
|
||||||
|
, Element.centerY
|
||||||
|
, Element.alignRight
|
||||||
|
]
|
||||||
|
, searchFill =
|
||||||
|
Color.light
|
||||||
|
++ Group.center
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -125,7 +209,8 @@ initialModel { viewport } =
|
|||||||
let
|
let
|
||||||
( scrollingNav, cmd ) =
|
( scrollingNav, cmd ) =
|
||||||
ScrollingNav.init
|
ScrollingNav.init
|
||||||
{ labels = Section.toString
|
{ toString = Section.toString
|
||||||
|
, fromString = Section.fromString
|
||||||
, arrangement = Section.asList
|
, arrangement = Section.asList
|
||||||
, toMsg = \result ->
|
, toMsg = \result ->
|
||||||
case result of
|
case result of
|
||||||
@ -141,13 +226,15 @@ initialModel { viewport } =
|
|||||||
, scrollingNav = scrollingNav
|
, scrollingNav = scrollingNav
|
||||||
, layout = Layout.init
|
, layout = Layout.init
|
||||||
, displayDialog = False
|
, displayDialog = False
|
||||||
, deviceClass =
|
, window =
|
||||||
{ width = viewport.width |> round
|
{ width = viewport.width |> round
|
||||||
, height = viewport.height |> round
|
, height = viewport.height |> round
|
||||||
}
|
}
|
||||||
|> Element.classifyDevice
|
, search =
|
||||||
|> .class
|
{ raw = ""
|
||||||
, search = ""
|
, current = ""
|
||||||
|
, remaining = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, cmd
|
, cmd
|
||||||
)
|
)
|
||||||
@ -171,25 +258,23 @@ view model =
|
|||||||
Layout.view []
|
Layout.view []
|
||||||
{ dialog =
|
{ dialog =
|
||||||
if m.displayDialog then
|
if m.displayDialog then
|
||||||
Just
|
{ body =
|
||||||
{ onDismiss = Just <| ToggleDialog False
|
"This is a dialog window"
|
||||||
, content =
|
|
||||||
[ Element.el Heading.h3 <| Element.text "Dialog"
|
|
||||||
, "This is a dialog window"
|
|
||||||
|> Element.text
|
|> Element.text
|
||||||
|> List.singleton
|
|> List.singleton
|
||||||
|> Element.paragraph []
|
|> Element.paragraph []
|
||||||
, Input.button (Button.simple ++ [ Element.alignRight ])
|
, title = Just "Dialog"
|
||||||
{ onPress = Just <| ToggleDialog False
|
, accept = Just
|
||||||
, label = Element.text "Ok"
|
{ text = "Ok"
|
||||||
}
|
, onPress = Just <| ToggleDialog False
|
||||||
]
|
|
||||||
|> Element.column
|
|
||||||
( Grid.simple
|
|
||||||
++ Card.large
|
|
||||||
++ [Element.centerX, Element.centerY]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
, dismiss = Just
|
||||||
|
{ text = "Dismiss"
|
||||||
|
, onPress = Just <| ToggleDialog False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> Widget.dialog style.dialog
|
||||||
|
|> Just
|
||||||
|
|
||||||
else
|
else
|
||||||
Nothing
|
Nothing
|
||||||
@ -198,11 +283,12 @@ view model =
|
|||||||
, [ m.scrollingNav
|
, [ m.scrollingNav
|
||||||
|> ScrollingNav.view
|
|> ScrollingNav.view
|
||||||
(\section ->
|
(\section ->
|
||||||
case section of
|
( case section of
|
||||||
ComponentViews ->
|
ComponentViews ->
|
||||||
m.component
|
m.component
|
||||||
|> Component.view
|
|> Component.view ComponentSpecific
|
||||||
|> Element.map ComponentSpecific
|
|
||||||
|
|
||||||
|
|
||||||
ReusableViews ->
|
ReusableViews ->
|
||||||
Reusable.view
|
Reusable.view
|
||||||
@ -218,6 +304,39 @@ view model =
|
|||||||
, changedSheet = ChangedSidebar
|
, changedSheet = ChangedSidebar
|
||||||
}
|
}
|
||||||
m.stateless
|
m.stateless
|
||||||
|
) |> (\{title,description,items} ->
|
||||||
|
[ Element.el Heading.h2 <| Element.text <| title
|
||||||
|
, if m.search.current == "" then
|
||||||
|
description
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
else Element.none
|
||||||
|
, items
|
||||||
|
|> (if m.search.current /= "" then
|
||||||
|
List.filter
|
||||||
|
( Tuple.first
|
||||||
|
>> String.toLower
|
||||||
|
>> String.contains (m.search.current |> String.toLower)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
identity)
|
||||||
|
|> List.map
|
||||||
|
(\(name,elem) ->
|
||||||
|
[ Element.text name
|
||||||
|
|> Element.el Heading.h3
|
||||||
|
, elem
|
||||||
|
]
|
||||||
|
|> Element.column
|
||||||
|
(Grid.simple
|
||||||
|
++ Card.large
|
||||||
|
++ [Element.height <| Element.fill])
|
||||||
|
)
|
||||||
|
|> Element.wrappedRow
|
||||||
|
(Grid.simple ++ [Element.height <| Element.shrink])
|
||||||
|
]
|
||||||
|
|> Element.column (Grid.section ++ [ Element.centerX ])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|> Element.column Framework.container
|
|> Element.column Framework.container
|
||||||
@ -225,52 +344,35 @@ view model =
|
|||||||
|> Element.column Grid.compact
|
|> Element.column Grid.compact
|
||||||
, style = style
|
, style = style
|
||||||
, layout = m.layout
|
, layout = m.layout
|
||||||
, deviceClass = m.deviceClass
|
, window = m.window
|
||||||
, menu =
|
, menu =
|
||||||
{ selected =
|
m.scrollingNav
|
||||||
Section.asList
|
|> ScrollingNav.toSelect
|
||||||
|> List.indexedMap (\i s -> (i,s))
|
(\int ->
|
||||||
|> List.filterMap
|
m.scrollingNav.arrangement
|
||||||
( \(i,s) ->
|
|> Array.fromList
|
||||||
if m.scrollingNav
|
|> Array.get int
|
||||||
|> ScrollingNav.current Section.fromString
|
|> Maybe.map JumpTo
|
||||||
|> (==) (Just s)
|
)
|
||||||
then
|
|
||||||
Just i
|
|
||||||
else
|
|
||||||
Nothing
|
|
||||||
)
|
|
||||||
|> List.head
|
|
||||||
|> Maybe.withDefault 0
|
|
||||||
, items =
|
|
||||||
Section.asList
|
|
||||||
|> List.map
|
|
||||||
(\label ->
|
|
||||||
{ icon = Element.none
|
|
||||||
, label = label |> Section.toString
|
|
||||||
, onPress = Just <| JumpTo <| label
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
, actions =
|
, actions =
|
||||||
[ { onPress = Just <| Load "https://package.elm-lang.org/packages/Orasund/elm-ui-widgets/latest/"
|
[ { onPress = Just <| Load "https://package.elm-lang.org/packages/Orasund/elm-ui-widgets/latest/"
|
||||||
, label = "Docs"
|
, text = "Docs"
|
||||||
, icon = Icons.book|> Element.html |> Element.el []
|
, icon = Icons.book|> Element.html |> Element.el []
|
||||||
}
|
}
|
||||||
, { onPress = Just <| Load "https://github.com/Orasund/elm-ui-widgets"
|
, { onPress = Just <| Load "https://github.com/Orasund/elm-ui-widgets"
|
||||||
, label = "Github"
|
, text = "Github"
|
||||||
, icon = Icons.github|> Element.html |> Element.el []
|
, icon = Icons.github|> Element.html |> Element.el []
|
||||||
}
|
}
|
||||||
, { onPress = Nothing
|
, { onPress = Nothing
|
||||||
, label = "Placeholder"
|
, text = "Placeholder"
|
||||||
, icon = Icons.circle|> Element.html |> Element.el []
|
, icon = Icons.circle|> Element.html |> Element.el []
|
||||||
}
|
}
|
||||||
, { onPress = Nothing
|
, { onPress = Nothing
|
||||||
, label = "Placeholder"
|
, text = "Placeholder"
|
||||||
, icon = Icons.triangle|> Element.html |> Element.el []
|
, icon = Icons.triangle|> Element.html |> Element.el []
|
||||||
}
|
}
|
||||||
, { onPress = Nothing
|
, { onPress = Nothing
|
||||||
, label = "Placeholder"
|
, text = "Placeholder"
|
||||||
, icon = Icons.square|> Element.html |> Element.el []
|
, icon = Icons.square|> Element.html |> Element.el []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -281,7 +383,7 @@ view model =
|
|||||||
|> Element.el Heading.h1
|
|> Element.el Heading.h1
|
||||||
, search =
|
, search =
|
||||||
Just
|
Just
|
||||||
{ text = m.search
|
{ text = m.search.raw
|
||||||
, onChange = ChangedSearch
|
, onChange = ChangedSearch
|
||||||
, label = "Search"
|
, label = "Search"
|
||||||
}
|
}
|
||||||
@ -330,15 +432,44 @@ updateLoaded msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
TimePassed int ->
|
TimePassed int ->
|
||||||
|
let
|
||||||
|
search = model.search
|
||||||
|
in
|
||||||
( { model
|
( { model
|
||||||
| layout = model.layout |> Layout.timePassed int
|
| layout = model.layout |> Layout.timePassed int
|
||||||
|
, search =
|
||||||
|
if search.remaining > 0 then
|
||||||
|
if search.remaining <= int then
|
||||||
|
{ search
|
||||||
|
| current = search.raw
|
||||||
|
, remaining = 0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ search
|
||||||
|
| remaining = search.remaining - int
|
||||||
|
}
|
||||||
|
else
|
||||||
|
model.search
|
||||||
}
|
}
|
||||||
, ScrollingNav.getPos
|
, ScrollingNav.getPos
|
||||||
|> Task.perform UpdateScrollingNav
|
|> Task.perform UpdateScrollingNav
|
||||||
)
|
)
|
||||||
|
|
||||||
AddSnackbar string ->
|
AddSnackbar (string,bool) ->
|
||||||
( { model | layout = model.layout |> Layout.queueMessage string }
|
( { model
|
||||||
|
| layout = model.layout
|
||||||
|
|> Layout.queueMessage
|
||||||
|
{ text = string
|
||||||
|
, button = if bool then
|
||||||
|
Just
|
||||||
|
{ text = "Add"
|
||||||
|
, onPress = Just <|
|
||||||
|
(AddSnackbar ("This is another message", False))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -347,8 +478,8 @@ updateLoaded msg model =
|
|||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
Resized screen ->
|
Resized window ->
|
||||||
( { model | deviceClass = screen |> Element.classifyDevice |> .class }
|
( { model | window = window }
|
||||||
, Cmd.none
|
, Cmd.none
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -370,7 +501,17 @@ updateLoaded msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
ChangedSearch string ->
|
ChangedSearch string ->
|
||||||
( { model | search = string},Cmd.none)
|
let
|
||||||
|
search = model.search
|
||||||
|
in
|
||||||
|
( { model | search =
|
||||||
|
{ search
|
||||||
|
| raw = string
|
||||||
|
, remaining = 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
Idle ->
|
Idle ->
|
||||||
( model , Cmd.none)
|
( model , Cmd.none)
|
||||||
|
@ -7,6 +7,7 @@ module Icons exposing
|
|||||||
, triangle
|
, triangle
|
||||||
, square
|
, square
|
||||||
, search
|
, search
|
||||||
|
, slash
|
||||||
)
|
)
|
||||||
|
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
@ -66,6 +67,13 @@ circle =
|
|||||||
[ Svg.circle [ cx "12", cy "12", r "10" ] []
|
[ Svg.circle [ cx "12", cy "12", r "10" ] []
|
||||||
]
|
]
|
||||||
|
|
||||||
|
slash : Html msg
|
||||||
|
slash =
|
||||||
|
svgFeatherIcon "slash"
|
||||||
|
[ Svg.circle [ cx "12", cy "12", r "10" ] []
|
||||||
|
, Svg.line [ x1 "4.93", y1 "4.93", x2 "19.07", y2 "19.07" ] []
|
||||||
|
]
|
||||||
|
|
||||||
triangle : Html msg
|
triangle : Html msg
|
||||||
triangle =
|
triangle =
|
||||||
svgFeatherIcon "triangle"
|
svgFeatherIcon "triangle"
|
||||||
|
@ -54,24 +54,37 @@ init =
|
|||||||
SortTable.sortBy { title = "Name", asc = True }
|
SortTable.sortBy { title = "Name", asc = True }
|
||||||
|
|
||||||
|
|
||||||
snackbar : (String -> msg) -> Element msg
|
snackbar : ((String,Bool) -> msg) -> (String,Element msg)
|
||||||
snackbar addSnackbar =
|
snackbar addSnackbar =
|
||||||
[ Element.el Heading.h3 <| Element.text "Snackbar"
|
( "Snackbar"
|
||||||
, Input.button Button.simple
|
, [Input.button Button.simple
|
||||||
{ onPress = Just <| addSnackbar "This is a notification. It will disappear after 10 seconds."
|
{ onPress = Just <| addSnackbar <|
|
||||||
|
("This is a notification. It will disappear after 10 seconds."
|
||||||
|
, False
|
||||||
|
)
|
||||||
, label =
|
, label =
|
||||||
"Add Notification"
|
"Add Notification"
|
||||||
|> Element.text
|
|> Element.text
|
||||||
|> List.singleton
|
|> List.singleton
|
||||||
|> Element.paragraph []
|
|> Element.paragraph []
|
||||||
}
|
}
|
||||||
]
|
, Input.button Button.simple
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
{ onPress = Just <| addSnackbar <|
|
||||||
|
("You can add another notification if you want."
|
||||||
|
, True
|
||||||
|
)
|
||||||
|
, label =
|
||||||
|
"Add Notification with Action"
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph []
|
||||||
|
}
|
||||||
|
] |> Element.column Grid.simple
|
||||||
|
)
|
||||||
|
|
||||||
|
sortTable : SortTable.Model -> (String,Element Msg)
|
||||||
sortTable : SortTable.Model -> Element Msg
|
|
||||||
sortTable model =
|
sortTable model =
|
||||||
[ Element.el Heading.h3 <| Element.text "Sort Table"
|
( "Sort Table"
|
||||||
, SortTable.view
|
, SortTable.view
|
||||||
{ content =
|
{ content =
|
||||||
[ { id = 1, name = "Antonio", rating = 2.456 }
|
[ { id = 1, name = "Antonio", rating = 2.456 }
|
||||||
@ -139,35 +152,31 @@ sortTable model =
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> Element.table Grid.simple
|
|> Element.table Grid.simple
|
||||||
]
|
)
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
|
||||||
|
|
||||||
scrollingNavCard : Element msg
|
scrollingNavCard : (String , Element msg )
|
||||||
scrollingNavCard =
|
scrollingNavCard =
|
||||||
[ Element.el Heading.h3 <| Element.text "Scrolling Nav"
|
("Scrolling Nav"
|
||||||
, Element.text "Resize the screen and open the side-menu. Then start scrolling to see the scrolling navigation in action."
|
, Element.text "Resize the screen and open the side-menu. Then start scrolling to see the scrolling navigation in action."
|
||||||
|> List.singleton
|
|> List.singleton
|
||||||
|> Element.paragraph []
|
|> Element.paragraph []
|
||||||
]
|
)
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
|
||||||
|
|
||||||
|
|
||||||
view :
|
view :
|
||||||
{ addSnackbar : String -> msg
|
{ addSnackbar : (String,Bool) -> msg
|
||||||
, msgMapper : Msg -> msg
|
, msgMapper : Msg -> msg
|
||||||
, model : Model
|
, model : Model
|
||||||
}
|
}
|
||||||
-> Element msg
|
-> { title : String
|
||||||
|
, description : String
|
||||||
|
, items : List (String,Element msg)
|
||||||
|
}
|
||||||
view { addSnackbar, msgMapper, model } =
|
view { addSnackbar, msgMapper, model } =
|
||||||
Element.column (Grid.section ++ [ Element.centerX ])
|
{ title = "Reusable Views"
|
||||||
[ Element.el Heading.h2 <| Element.text "Reusable Views"
|
, description = "Reusable views have an internal state but no update function. You will need to do some wiring, but nothing complicated."
|
||||||
, "Reusable views have an internal state but no update function. You will need to do some wiring, but nothing complicated."
|
, items =
|
||||||
|> Element.text
|
[ snackbar addSnackbar
|
||||||
|> List.singleton
|
, sortTable model |> Tuple.mapSecond (Element.map msgMapper)
|
||||||
|> Element.paragraph []
|
, scrollingNavCard
|
||||||
, Element.wrappedRow (Grid.simple ++ [Element.height <| Element.shrink]) <|
|
|
||||||
[ snackbar addSnackbar
|
|
||||||
, sortTable model |> Element.map msgMapper
|
|
||||||
, scrollingNavCard
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
@ -17,16 +17,34 @@ import Heroicons.Solid as Heroicons
|
|||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Html.Attributes as Attributes
|
import Html.Attributes as Attributes
|
||||||
import Set exposing (Set)
|
import Set exposing (Set)
|
||||||
import Widget
|
import Widget.Button as Button exposing (ButtonStyle)
|
||||||
import Layout exposing (Part(..))
|
import Layout exposing (Part(..))
|
||||||
|
import Icons
|
||||||
|
import Widget
|
||||||
|
|
||||||
|
buttonStyle : ButtonStyle msg
|
||||||
|
buttonStyle =
|
||||||
|
{ label = [ Element.spacing 8]
|
||||||
|
, container = Button.simple
|
||||||
|
, disabled = Color.disabled
|
||||||
|
, active = Color.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
tabButtonStyle :ButtonStyle msg
|
||||||
|
tabButtonStyle=
|
||||||
|
{ label = [ Element.spacing 8]
|
||||||
|
, container = Button.simple ++ Group.top
|
||||||
|
, disabled = Color.disabled
|
||||||
|
, active = Color.primary
|
||||||
|
}
|
||||||
|
|
||||||
type alias Model =
|
type alias Model =
|
||||||
{ selected : Maybe Int
|
{ selected : Maybe Int
|
||||||
, multiSelected : Set Int
|
, multiSelected : Set Int
|
||||||
, isCollapsed : Bool
|
, isCollapsed : Bool
|
||||||
, carousel : Int
|
, carousel : Int
|
||||||
, tab : Int
|
, tab : Maybe Int
|
||||||
|
, button : Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -36,6 +54,7 @@ type Msg
|
|||||||
| ToggleCollapsable Bool
|
| ToggleCollapsable Bool
|
||||||
| ChangedTab Int
|
| ChangedTab Int
|
||||||
| SetCarousel Int
|
| SetCarousel Int
|
||||||
|
| ToggleButton Bool
|
||||||
|
|
||||||
|
|
||||||
init : Model
|
init : Model
|
||||||
@ -44,7 +63,8 @@ init =
|
|||||||
, multiSelected = Set.empty
|
, multiSelected = Set.empty
|
||||||
, isCollapsed = False
|
, isCollapsed = False
|
||||||
, carousel = 0
|
, carousel = 0
|
||||||
, tab = 1
|
, tab = Just 1
|
||||||
|
, button = True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -91,35 +111,31 @@ update msg model =
|
|||||||
)
|
)
|
||||||
|
|
||||||
ChangedTab int ->
|
ChangedTab int ->
|
||||||
( { model | tab = int }, Cmd.none )
|
( { model | tab = Just int }, Cmd.none )
|
||||||
|
|
||||||
|
ToggleButton bool ->
|
||||||
|
( { model | button = bool }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
select : Model -> Element Msg
|
select : Model -> (String,Element Msg)
|
||||||
select model =
|
select model =
|
||||||
[ Element.el Heading.h3 <| Element.text "Select"
|
( "Select"
|
||||||
, Widget.select
|
, { selected = model.selected
|
||||||
{ selected = model.selected
|
, options =
|
||||||
, options = [ 1, 2, 42 ]
|
[ 1, 2, 42 ]
|
||||||
, label = String.fromInt >> Element.text
|
|> List.map (\int ->
|
||||||
, onChange = ChangedSelected
|
{ text = String.fromInt int
|
||||||
, attributes =
|
, icon = Element.none
|
||||||
\selected ->
|
}
|
||||||
Button.simple
|
)
|
||||||
++ [ Border.width 0
|
, onSelect = ChangedSelected >> Just
|
||||||
, Border.rounded 0
|
}
|
||||||
]
|
|> Widget.select
|
||||||
++ (if selected then
|
|
||||||
Color.primary
|
|
||||||
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|> List.indexedMap
|
|> List.indexedMap
|
||||||
(\i ->
|
(\i ->
|
||||||
Element.el
|
Widget.selectButton
|
||||||
(Button.simple
|
{ buttonStyle
|
||||||
++ [ Element.padding <| 0 ]
|
| container = buttonStyle.container
|
||||||
++ (if i == 0 then
|
++ (if i == 0 then
|
||||||
Group.left
|
Group.left
|
||||||
|
|
||||||
@ -129,39 +145,31 @@ select model =
|
|||||||
else
|
else
|
||||||
Group.center
|
Group.center
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|> Element.row Grid.compact
|
|> Element.row Grid.compact
|
||||||
]
|
)
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
|
||||||
|
|
||||||
|
|
||||||
multiSelect : Model -> Element Msg
|
multiSelect : Model -> (String,Element Msg)
|
||||||
multiSelect model =
|
multiSelect model =
|
||||||
[ Element.el Heading.h3 <| Element.text "Multi Select"
|
( "Multi Select"
|
||||||
, Widget.multiSelect
|
, { selected = model.multiSelected
|
||||||
{ selected = model.multiSelected
|
, options =
|
||||||
, options = [ 1, 2, 42 ]
|
[ 1, 2, 42 ]
|
||||||
, label = String.fromInt >> Element.text
|
|> List.map (\int ->
|
||||||
, onChange = ChangedMultiSelected
|
{ text = String.fromInt int
|
||||||
, attributes =
|
, icon = Element.none
|
||||||
\selected ->
|
})
|
||||||
Button.simple
|
, onSelect = ChangedMultiSelected >> Just
|
||||||
++ [ Border.width 0
|
}
|
||||||
, Border.rounded 0
|
|> Widget.multiSelect
|
||||||
]
|
|
||||||
++ (if selected then
|
|
||||||
Color.primary
|
|
||||||
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|> List.indexedMap
|
|> List.indexedMap
|
||||||
(\i ->
|
(\i ->
|
||||||
Element.el
|
Widget.selectButton
|
||||||
(Button.simple
|
{ buttonStyle
|
||||||
++ [ Element.padding <| 0 ]
|
| container = buttonStyle.container
|
||||||
++ (if i == 0 then
|
++ (if i == 0 then
|
||||||
Group.left
|
Group.left
|
||||||
|
|
||||||
@ -171,16 +179,14 @@ multiSelect model =
|
|||||||
else
|
else
|
||||||
Group.center
|
Group.center
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
)
|
)
|
||||||
|> Element.row Grid.compact
|
|> Element.row Grid.compact
|
||||||
]
|
)
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
|
||||||
|
|
||||||
|
collapsable : Model -> (String,Element Msg)
|
||||||
collapsable : Model -> Element Msg
|
|
||||||
collapsable model =
|
collapsable model =
|
||||||
[ Element.el Heading.h3 <| Element.text "Collapsable"
|
( "Collapsable"
|
||||||
, Widget.collapsable
|
, Widget.collapsable
|
||||||
{ onToggle = ToggleCollapsable
|
{ onToggle = ToggleCollapsable
|
||||||
, isCollapsed = model.isCollapsed
|
, isCollapsed = model.isCollapsed
|
||||||
@ -196,28 +202,33 @@ collapsable model =
|
|||||||
]
|
]
|
||||||
, content = Element.text <| "Hello World"
|
, content = Element.text <| "Hello World"
|
||||||
}
|
}
|
||||||
]
|
)
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
|
||||||
|
|
||||||
|
tab : Model -> (String,Element Msg)
|
||||||
tab : Model -> Element Msg
|
|
||||||
tab model =
|
tab model =
|
||||||
[ Element.el Heading.h3 <| Element.text "Tab"
|
( "Tab"
|
||||||
, Widget.tab Grid.simple
|
, Widget.tab
|
||||||
{ selected = model.tab
|
{ tabButton = tabButtonStyle
|
||||||
, options = [ 1, 2, 3 ]
|
, tabRow = Grid.simple
|
||||||
, onChange = ChangedTab
|
}
|
||||||
, label = \int -> "Tab " ++ String.fromInt int |> Element.text
|
{ selected = model.tab
|
||||||
, content =
|
, options = [ 1, 2, 3 ]
|
||||||
\selected ->
|
|> List.map (\int ->
|
||||||
|
{ text = "Tab " ++ (int |> String.fromInt)
|
||||||
|
, icon = Element.none
|
||||||
|
}
|
||||||
|
)
|
||||||
|
, onSelect = ChangedTab >> Just
|
||||||
|
} <|
|
||||||
|
(\selected ->
|
||||||
(case selected of
|
(case selected of
|
||||||
1 ->
|
Just 0 ->
|
||||||
"This is Tab 1"
|
"This is Tab 1"
|
||||||
|
|
||||||
2 ->
|
Just 1 ->
|
||||||
"This is the second tab"
|
"This is the second tab"
|
||||||
|
|
||||||
3 ->
|
Just 2 ->
|
||||||
"The thrid and last tab"
|
"The thrid and last tab"
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
@ -225,46 +236,35 @@ tab model =
|
|||||||
)
|
)
|
||||||
|> Element.text
|
|> Element.text
|
||||||
|> Element.el (Card.small ++ Group.bottom)
|
|> Element.el (Card.small ++ Group.bottom)
|
||||||
, attributes =
|
)
|
||||||
\selected ->
|
)
|
||||||
Button.simple
|
|
||||||
++ Group.top
|
|
||||||
++ (if selected then
|
|
||||||
Color.primary
|
|
||||||
|
|
||||||
else
|
modal : (Maybe Part -> msg) -> Model -> (String,Element msg)
|
||||||
[]
|
modal changedSheet model =
|
||||||
)
|
( "Modal"
|
||||||
}
|
, [ Input.button Button.simple
|
||||||
]
|
{ onPress = Just <| changedSheet <| Just LeftSheet
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
, label = Element.text <| "show left sheet"
|
||||||
|
}
|
||||||
|
, Input.button Button.simple
|
||||||
|
{ onPress = Just <| changedSheet <| Just RightSheet
|
||||||
|
, label = Element.text <| "show right sheet"
|
||||||
|
}
|
||||||
|
] |> Element.column Grid.simple
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog : msg -> Model -> (String,Element msg)
|
||||||
scrim :
|
dialog showDialog model =
|
||||||
{ showDialog : msg
|
( "Dialog"
|
||||||
, changedSheet : Maybe Part -> msg
|
|
||||||
} -> Model -> Element msg
|
|
||||||
scrim {showDialog,changedSheet} model =
|
|
||||||
[ Element.el Heading.h3 <| Element.text "Scrim"
|
|
||||||
, Input.button Button.simple
|
, Input.button Button.simple
|
||||||
{ onPress = Just showDialog
|
{ onPress = Just showDialog
|
||||||
, label = Element.text <| "Show dialog"
|
, label = Element.text <| "Show dialog"
|
||||||
}
|
}
|
||||||
, Input.button Button.simple
|
)
|
||||||
{ onPress = Just <| changedSheet <| Just LeftSheet
|
|
||||||
, label = Element.text <| "show left sheet"
|
|
||||||
}
|
|
||||||
, Input.button Button.simple
|
|
||||||
{ onPress = Just <| changedSheet <| Just RightSheet
|
|
||||||
, label = Element.text <| "show right sheet"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
|
||||||
|
|
||||||
|
carousel : Model -> (String,Element Msg)
|
||||||
carousel : Model -> Element Msg
|
|
||||||
carousel model =
|
carousel model =
|
||||||
[ Element.el Heading.h3 <| Element.text "Carousel"
|
( "Carousel"
|
||||||
, Widget.carousel
|
, Widget.carousel
|
||||||
{ content = ( Color.cyan, [ Color.yellow, Color.green, Color.red ] |> Array.fromList )
|
{ content = ( Color.cyan, [ Color.yellow, Color.green, Color.red ] |> Array.fromList )
|
||||||
, current = model.carousel
|
, current = model.carousel
|
||||||
@ -294,33 +294,47 @@ carousel model =
|
|||||||
]
|
]
|
||||||
|> Element.row (Grid.simple ++ [ Element.centerX, Element.width <| Element.shrink ])
|
|> Element.row (Grid.simple ++ [ Element.centerX, Element.width <| Element.shrink ])
|
||||||
}
|
}
|
||||||
]
|
)
|
||||||
|> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill])
|
|
||||||
|
|
||||||
|
iconButton : Model -> (String,Element Msg)
|
||||||
|
iconButton model =
|
||||||
|
( "Icon Button"
|
||||||
|
, [Button.view buttonStyle
|
||||||
|
{ text = "disable me"
|
||||||
|
, icon = Icons.slash |> Element.html |> Element.el [] , onPress =
|
||||||
|
if model.button then
|
||||||
|
Just <| ToggleButton False
|
||||||
|
else
|
||||||
|
Nothing
|
||||||
|
}
|
||||||
|
, Button.view buttonStyle
|
||||||
|
{ text = "reset button"
|
||||||
|
, icon = Element.none
|
||||||
|
, onPress = Just <| ToggleButton True
|
||||||
|
}
|
||||||
|
] |> Element.column Grid.simple
|
||||||
|
)
|
||||||
|
|
||||||
view :
|
view :
|
||||||
{ msgMapper : Msg -> msg
|
{ msgMapper : Msg -> msg
|
||||||
, showDialog : msg
|
, showDialog : msg
|
||||||
, changedSheet : Maybe Part -> msg
|
, changedSheet : Maybe Part -> msg
|
||||||
} -> Model -> Element msg
|
} -> Model
|
||||||
|
-> { title : String
|
||||||
|
, description : String
|
||||||
|
, items : List (String,Element msg)
|
||||||
|
}
|
||||||
view { msgMapper, showDialog, changedSheet } model =
|
view { msgMapper, showDialog, changedSheet } model =
|
||||||
Element.column (Grid.section )
|
{ title = "Stateless Views"
|
||||||
[ Element.el Heading.h2 <| Element.text "Stateless Views"
|
, description = "Stateless views are simple functions that view some content. No wiring required."
|
||||||
, "Stateless views are simple functions that view some content. No wiring required."
|
, items =
|
||||||
|> Element.text
|
[ iconButton model |> Tuple.mapSecond (Element.map msgMapper)
|
||||||
|> List.singleton
|
, select model |> Tuple.mapSecond (Element.map msgMapper)
|
||||||
|> Element.paragraph []
|
, multiSelect model |> Tuple.mapSecond (Element.map msgMapper)
|
||||||
, Element.wrappedRow
|
, collapsable model |> Tuple.mapSecond (Element.map msgMapper)
|
||||||
(Grid.simple ++ [Element.height <| Element.shrink])
|
, modal changedSheet model
|
||||||
<|
|
, carousel model |> Tuple.mapSecond (Element.map msgMapper)
|
||||||
[ select model |> Element.map msgMapper
|
, tab model |> Tuple.mapSecond (Element.map msgMapper)
|
||||||
, multiSelect model |> Element.map msgMapper
|
, dialog showDialog model
|
||||||
, collapsable model |> Element.map msgMapper
|
|
||||||
, scrim
|
|
||||||
{ showDialog = showDialog
|
|
||||||
, changedSheet = changedSheet
|
|
||||||
} model
|
|
||||||
, carousel model |> Element.map msgMapper
|
|
||||||
, tab model |> Element.map msgMapper
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
@ -1,112 +1,49 @@
|
|||||||
module Core.Style exposing (Style, menuButton, menuButtonSelected, menuIconButton, menuTabButton, menuTabButtonSelected, sheetButton, sheetButtonSelected)
|
module Core.Style exposing (Style, menuButton, menuIconButton, menuTabButton, sheetButton)
|
||||||
|
|
||||||
import Element exposing (Attribute, Element)
|
import Element exposing (Attribute, Element)
|
||||||
import Element.Input as Input
|
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
|
import Widget
|
||||||
|
import Widget.Button as Button exposing (Button, ButtonStyle)
|
||||||
|
|
||||||
|
|
||||||
type alias Style msg =
|
type alias Style style msg =
|
||||||
{ snackbar : List (Attribute msg)
|
{ style
|
||||||
, layout : List (Attribute msg) -> Element msg -> Html msg
|
| snackbar :
|
||||||
, header : List (Attribute msg)
|
{ row : List (Attribute msg)
|
||||||
, sheet : List (Attribute msg)
|
, text : List (Attribute msg)
|
||||||
, menuButton : List (Attribute msg)
|
, button : ButtonStyle msg
|
||||||
, menuButtonSelected : List (Attribute msg)
|
}
|
||||||
, sheetButton : List (Attribute msg)
|
, layout : List (Attribute msg) -> Element msg -> Html msg
|
||||||
, sheetButtonSelected : List (Attribute msg)
|
, header : List (Attribute msg)
|
||||||
, tabButton : List (Attribute msg)
|
, sheet : List (Attribute msg)
|
||||||
, tabButtonSelected : List (Attribute msg)
|
, sheetButton : ButtonStyle msg
|
||||||
, menuIcon : Element Never
|
, menuButton : ButtonStyle msg
|
||||||
, moreVerticalIcon : Element Never
|
, menuTabButton : ButtonStyle msg
|
||||||
, spacing : Int
|
, menuIcon : Element Never
|
||||||
, title : List (Attribute msg)
|
, moreVerticalIcon : Element Never
|
||||||
, searchIcon : Element Never
|
, spacing : Int
|
||||||
, search : List (Attribute msg)
|
, title : List (Attribute msg)
|
||||||
|
, searchIcon : Element Never
|
||||||
|
, search : List (Attribute msg)
|
||||||
|
, searchFill : List (Attribute msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type alias ButtonInfo msg =
|
menuButton : Style style msg -> ( Bool, Button msg ) -> Element msg
|
||||||
{ label : String
|
menuButton style =
|
||||||
, icon : Element Never
|
Widget.selectButton style.menuButton
|
||||||
, onPress : Maybe msg
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
menuButtonSelected : Style msg -> ButtonInfo msg -> Element msg
|
menuIconButton : Style style msg -> Button msg -> Element msg
|
||||||
menuButtonSelected config { label, icon, onPress } =
|
menuIconButton style =
|
||||||
Input.button (config.menuButton ++ config.menuButtonSelected)
|
Button.viewIconOnly style.menuButton
|
||||||
{ onPress = onPress
|
|
||||||
, label =
|
|
||||||
Element.row [ Element.spacing 8 ]
|
|
||||||
[ icon |> Element.map never
|
|
||||||
, Element.text label
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
menuButton : Style msg -> ButtonInfo msg -> Element msg
|
sheetButton : Style style msg -> ( Bool, Button msg ) -> Element msg
|
||||||
menuButton config { label, icon, onPress } =
|
sheetButton style =
|
||||||
Input.button config.menuButton
|
Widget.selectButton style.sheetButton
|
||||||
{ onPress = onPress
|
|
||||||
, label =
|
|
||||||
Element.row [ Element.spacing 8 ]
|
|
||||||
[ icon |> Element.map never
|
|
||||||
, Element.text label
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
menuIconButton : Style msg -> ButtonInfo msg -> Element msg
|
menuTabButton : Style style msg -> ( Bool, Button msg ) -> Element msg
|
||||||
menuIconButton config { label, icon, onPress } =
|
menuTabButton style =
|
||||||
Input.button config.menuButton
|
Widget.selectButton style.menuTabButton
|
||||||
{ onPress = onPress
|
|
||||||
, label = icon |> Element.map never
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sheetButton : Style msg -> ButtonInfo msg -> Element msg
|
|
||||||
sheetButton config { label, icon, onPress } =
|
|
||||||
Input.button config.sheetButton
|
|
||||||
{ onPress = onPress
|
|
||||||
, label =
|
|
||||||
Element.row [ Element.spacing 8 ]
|
|
||||||
[ icon |> Element.map never
|
|
||||||
, Element.text label
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sheetButtonSelected : Style msg -> ButtonInfo msg -> Element msg
|
|
||||||
sheetButtonSelected config { label, icon, onPress } =
|
|
||||||
Input.button (config.sheetButton ++ config.sheetButtonSelected)
|
|
||||||
{ onPress = onPress
|
|
||||||
, label =
|
|
||||||
Element.row [ Element.spacing 8 ]
|
|
||||||
[ icon |> Element.map never
|
|
||||||
, Element.text label
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
menuTabButton : Style msg -> ButtonInfo msg -> Element msg
|
|
||||||
menuTabButton config { label, icon, onPress } =
|
|
||||||
Input.button (config.menuButton ++ config.tabButton)
|
|
||||||
{ onPress = onPress
|
|
||||||
, label =
|
|
||||||
Element.row [ Element.spacing 8 ]
|
|
||||||
[ icon |> Element.map never
|
|
||||||
, Element.text label
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
menuTabButtonSelected : Style msg -> ButtonInfo msg -> Element msg
|
|
||||||
menuTabButtonSelected config { label, icon, onPress } =
|
|
||||||
Input.button (config.menuButton ++ config.tabButton ++ config.tabButtonSelected)
|
|
||||||
{ onPress = onPress
|
|
||||||
, label =
|
|
||||||
Element.row [ Element.spacing 8 ]
|
|
||||||
[ icon |> Element.map never
|
|
||||||
, Element.text label
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
185
src/Layout.elm
185
src/Layout.elm
@ -1,16 +1,13 @@
|
|||||||
module Layout exposing (Layout, Part(..), activate, init, queueMessage, timePassed, view)
|
module Layout exposing (Layout, Part(..), activate, init, queueMessage, timePassed, view)
|
||||||
|
|
||||||
import Array
|
import Array
|
||||||
import Browser.Dom exposing (Viewport)
|
|
||||||
import Core.Style as Style exposing (Style)
|
import Core.Style as Style exposing (Style)
|
||||||
import Element exposing (Attribute, DeviceClass(..), Element)
|
import Element exposing (Attribute, DeviceClass(..), Element)
|
||||||
import Element.Background as Background
|
|
||||||
import Element.Events as Events
|
|
||||||
import Element.Input as Input
|
import Element.Input as Input
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Html.Attributes as Attributes
|
import Widget exposing (Select)
|
||||||
import Widget
|
import Widget.Button as Button exposing (Button)
|
||||||
import Widget.Snackbar as Snackbar
|
import Widget.Snackbar as Snackbar exposing (Message)
|
||||||
|
|
||||||
|
|
||||||
type Part
|
type Part
|
||||||
@ -19,34 +16,34 @@ type Part
|
|||||||
| Search
|
| Search
|
||||||
|
|
||||||
|
|
||||||
type alias Layout =
|
type alias Layout msg =
|
||||||
{ snackbar : Snackbar.Model String
|
{ snackbar : Snackbar.Model (Message msg)
|
||||||
, active : Maybe Part
|
, active : Maybe Part
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init : Layout
|
init : Layout msg
|
||||||
init =
|
init =
|
||||||
{ snackbar = Snackbar.init
|
{ snackbar = Snackbar.init
|
||||||
, active = Nothing
|
, active = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
queueMessage : String -> Layout -> Layout
|
queueMessage : Message msg -> Layout msg -> Layout msg
|
||||||
queueMessage message layout =
|
queueMessage message layout =
|
||||||
{ layout
|
{ layout
|
||||||
| snackbar = layout.snackbar |> Snackbar.insert message
|
| snackbar = layout.snackbar |> Snackbar.insert message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
activate : Maybe Part -> Layout -> Layout
|
activate : Maybe Part -> Layout msg -> Layout msg
|
||||||
activate part layout =
|
activate part layout =
|
||||||
{ layout
|
{ layout
|
||||||
| active = part
|
| active = part
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
timePassed : Int -> Layout -> Layout
|
timePassed : Int -> Layout msg -> Layout msg
|
||||||
timePassed sec layout =
|
timePassed sec layout =
|
||||||
case layout.active of
|
case layout.active of
|
||||||
Just LeftSheet ->
|
Just LeftSheet ->
|
||||||
@ -64,28 +61,31 @@ timePassed sec layout =
|
|||||||
view :
|
view :
|
||||||
List (Attribute msg)
|
List (Attribute msg)
|
||||||
->
|
->
|
||||||
{ deviceClass : DeviceClass
|
{ window : { height : Int, width : Int }
|
||||||
, dialog : Maybe { onDismiss : Maybe msg, content : Element msg }
|
, dialog : Maybe { onDismiss : Maybe msg, content : Element msg }
|
||||||
, content : Element msg
|
, content : Element msg
|
||||||
, layout : Layout
|
, layout : Layout msg
|
||||||
, title : Element msg
|
, title : Element msg
|
||||||
, menu :
|
, menu : Select msg
|
||||||
{ selected : Int
|
|
||||||
, items : List { label : String, icon : Element Never, onPress : Maybe msg }
|
|
||||||
}
|
|
||||||
, search :
|
, search :
|
||||||
Maybe
|
Maybe
|
||||||
{ onChange : String -> msg
|
{ onChange : String -> msg
|
||||||
, text : String
|
, text : String
|
||||||
, label : String
|
, label : String
|
||||||
}
|
}
|
||||||
, actions : List { label : String, icon : Element Never, onPress : Maybe msg }
|
, actions : List (Button msg)
|
||||||
, onChangedSidebar : Maybe Part -> msg
|
, onChangedSidebar : Maybe Part -> msg
|
||||||
, style : Style msg
|
, style : Style style msg
|
||||||
}
|
}
|
||||||
-> Html msg
|
-> Html msg
|
||||||
view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, dialog, content, style, layout } =
|
view attributes { search, title, onChangedSidebar, menu, actions, window, dialog, content, style, layout } =
|
||||||
let
|
let
|
||||||
|
deviceClass : DeviceClass
|
||||||
|
deviceClass =
|
||||||
|
window
|
||||||
|
|> Element.classifyDevice
|
||||||
|
|> .class
|
||||||
|
|
||||||
( primaryActions, moreActions ) =
|
( primaryActions, moreActions ) =
|
||||||
( if (actions |> List.length) > 4 then
|
( if (actions |> List.length) > 4 then
|
||||||
actions |> List.take 2
|
actions |> List.take 2
|
||||||
@ -115,30 +115,29 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
[ (if
|
[ (if
|
||||||
(deviceClass == Phone)
|
(deviceClass == Phone)
|
||||||
|| (deviceClass == Tablet)
|
|| (deviceClass == Tablet)
|
||||||
|| ((menu.items |> List.length) > 5)
|
|| ((menu.options |> List.length) > 5)
|
||||||
then
|
then
|
||||||
[ Input.button style.menuButton
|
[ Button.viewIconOnly style.menuButton
|
||||||
{ onPress = Just <| onChangedSidebar <| Just LeftSheet
|
{ onPress = Just <| onChangedSidebar <| Just LeftSheet
|
||||||
, label = style.menuIcon |> Element.map never
|
, icon = style.menuIcon |> Element.map never
|
||||||
|
, text = "Menu"
|
||||||
}
|
}
|
||||||
, menu.items
|
, menu.selected
|
||||||
|> Array.fromList
|
|> Maybe.andThen
|
||||||
|> Array.get menu.selected
|
(\option ->
|
||||||
|> Maybe.map (.label >> Element.text >> Element.el style.title)
|
menu.options
|
||||||
|
|> Array.fromList
|
||||||
|
|> Array.get option
|
||||||
|
)
|
||||||
|
|> Maybe.map (.text >> Element.text >> Element.el style.title)
|
||||||
|> Maybe.withDefault title
|
|> Maybe.withDefault title
|
||||||
]
|
]
|
||||||
|
|
||||||
else
|
else
|
||||||
[ title
|
[ title
|
||||||
, menu.items
|
, menu
|
||||||
|> List.indexedMap
|
|> Widget.select
|
||||||
(\i ->
|
|> List.map (Style.menuTabButton style)
|
||||||
if i == menu.selected then
|
|
||||||
Style.menuTabButtonSelected style
|
|
||||||
|
|
||||||
else
|
|
||||||
Style.menuTabButton style
|
|
||||||
)
|
|
||||||
|> Element.row
|
|> Element.row
|
||||||
[ Element.width <| Element.shrink
|
[ Element.width <| Element.shrink
|
||||||
]
|
]
|
||||||
@ -146,9 +145,9 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
)
|
)
|
||||||
|> Element.row
|
|> Element.row
|
||||||
[ Element.width <| Element.shrink
|
[ Element.width <| Element.shrink
|
||||||
, Element.spacing 8
|
, Element.spacing style.spacing
|
||||||
]
|
]
|
||||||
, if deviceClass == Phone then
|
, if deviceClass == Phone || deviceClass == Tablet then
|
||||||
Element.none
|
Element.none
|
||||||
|
|
||||||
else
|
else
|
||||||
@ -166,37 +165,45 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> Maybe.withDefault Element.none
|
|> Maybe.withDefault Element.none
|
||||||
, [ if deviceClass == Phone then
|
, [ search
|
||||||
search
|
|> Maybe.map
|
||||||
|> Maybe.map
|
(\{ label } ->
|
||||||
(\{ label } ->
|
if deviceClass == Tablet then
|
||||||
[ Style.menuButton style
|
[ Button.view style.menuButton
|
||||||
{ onPress = Just <| onChangedSidebar <| Just Search
|
{ onPress = Just <| onChangedSidebar <| Just Search
|
||||||
, icon = style.searchIcon
|
, icon = style.searchIcon
|
||||||
, label = label
|
, text = label
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
|
||||||
|> Maybe.withDefault []
|
|
||||||
|
|
||||||
else
|
else if deviceClass == Phone then
|
||||||
[]
|
[ Style.menuIconButton style
|
||||||
|
{ onPress = Just <| onChangedSidebar <| Just Search
|
||||||
|
, icon = style.searchIcon
|
||||||
|
, text = label
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|> Maybe.withDefault []
|
||||||
, primaryActions
|
, primaryActions
|
||||||
|> List.map
|
|> List.map
|
||||||
(if deviceClass == Phone then
|
(if deviceClass == Phone then
|
||||||
Style.menuIconButton style
|
Style.menuIconButton style
|
||||||
|
|
||||||
else
|
else
|
||||||
Style.menuButton style
|
Button.view style.menuButton
|
||||||
)
|
)
|
||||||
, if moreActions |> List.isEmpty then
|
, if moreActions |> List.isEmpty then
|
||||||
[]
|
[]
|
||||||
|
|
||||||
else
|
else
|
||||||
[ Style.menuButton style
|
[ Button.viewIconOnly style.menuButton
|
||||||
{ onPress = Just <| onChangedSidebar <| Just RightSheet
|
{ onPress = Just <| onChangedSidebar <| Just RightSheet
|
||||||
, icon = style.moreVerticalIcon
|
, icon = style.moreVerticalIcon
|
||||||
, label = ""
|
, text = "More"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -210,7 +217,7 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
(style.header
|
(style.header
|
||||||
++ [ Element.padding 0
|
++ [ Element.padding 0
|
||||||
, Element.centerX
|
, Element.centerX
|
||||||
, Element.spaceEvenly
|
, Element.spacing style.spacing
|
||||||
, Element.alignTop
|
, Element.alignTop
|
||||||
, Element.width <| Element.fill
|
, Element.width <| Element.fill
|
||||||
]
|
]
|
||||||
@ -218,16 +225,13 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
|
|
||||||
snackbar =
|
snackbar =
|
||||||
layout.snackbar
|
layout.snackbar
|
||||||
|> Snackbar.current
|
|> Snackbar.view style.snackbar identity
|
||||||
|> Maybe.map
|
|> Maybe.map
|
||||||
(Element.text
|
(Element.el
|
||||||
>> List.singleton
|
[ Element.padding style.spacing
|
||||||
>> Element.paragraph style.snackbar
|
, Element.alignBottom
|
||||||
>> Element.el
|
, Element.alignRight
|
||||||
[ Element.padding 8
|
]
|
||||||
, Element.alignBottom
|
|
||||||
, Element.alignRight
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|> Maybe.withDefault Element.none
|
|> Maybe.withDefault Element.none
|
||||||
|
|
||||||
@ -236,15 +240,9 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
Just LeftSheet ->
|
Just LeftSheet ->
|
||||||
[ [ title
|
[ [ title
|
||||||
]
|
]
|
||||||
, menu.items
|
, menu
|
||||||
|> List.indexedMap
|
|> Widget.select
|
||||||
(\i ->
|
|> List.map (Style.sheetButton style)
|
||||||
if i == menu.selected then
|
|
||||||
Style.sheetButtonSelected style
|
|
||||||
|
|
||||||
else
|
|
||||||
Style.sheetButton style
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
|> List.concat
|
|> List.concat
|
||||||
|> Element.column [ Element.width <| Element.fill ]
|
|> Element.column [ Element.width <| Element.fill ]
|
||||||
@ -257,7 +255,7 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
|
|
||||||
Just RightSheet ->
|
Just RightSheet ->
|
||||||
moreActions
|
moreActions
|
||||||
|> List.map (Style.sheetButton style)
|
|> List.map (Button.view style.sheetButton)
|
||||||
|> Element.column [ Element.width <| Element.fill ]
|
|> Element.column [ Element.width <| Element.fill ]
|
||||||
|> Element.el
|
|> Element.el
|
||||||
(style.sheet
|
(style.sheet
|
||||||
@ -269,7 +267,11 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
Just Search ->
|
Just Search ->
|
||||||
case search of
|
case search of
|
||||||
Just { onChange, text, label } ->
|
Just { onChange, text, label } ->
|
||||||
Input.text style.search
|
Input.text
|
||||||
|
(style.searchFill
|
||||||
|
++ [ Element.width <| Element.fill
|
||||||
|
]
|
||||||
|
)
|
||||||
{ onChange = onChange
|
{ onChange = onChange
|
||||||
, text = text
|
, text = text
|
||||||
, placeholder =
|
, placeholder =
|
||||||
@ -297,33 +299,22 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d
|
|||||||
, Element.inFront snackbar
|
, Element.inFront snackbar
|
||||||
]
|
]
|
||||||
, if (layout.active /= Nothing) || (dialog /= Nothing) then
|
, if (layout.active /= Nothing) || (dialog /= Nothing) then
|
||||||
Widget.scrim
|
(Element.height <| Element.px <| window.height)
|
||||||
{ onDismiss =
|
:: (case dialog of
|
||||||
Just <|
|
Just dialogConfig ->
|
||||||
case dialog of
|
Widget.modal dialogConfig
|
||||||
Just { onDismiss } ->
|
|
||||||
onDismiss
|
|
||||||
|> Maybe.withDefault
|
|
||||||
(Nothing
|
|
||||||
|> onChangedSidebar
|
|
||||||
)
|
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Nothing
|
Widget.modal
|
||||||
|> onChangedSidebar
|
{ onDismiss =
|
||||||
, content = Element.none
|
Nothing
|
||||||
}
|
|> onChangedSidebar
|
||||||
|
|> Just
|
||||||
|
, content = sheet
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
, [ Element.inFront sheet
|
|
||||||
, Element.inFront <|
|
|
||||||
case dialog of
|
|
||||||
Just element ->
|
|
||||||
element.content
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
Element.none
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
262
src/Widget.elm
262
src/Widget.elm
@ -1,11 +1,11 @@
|
|||||||
module Widget exposing
|
module Widget exposing
|
||||||
( select, multiSelect, collapsable, carousel, scrim, tab
|
( select, multiSelect, collapsable, carousel, modal, tab, dialog
|
||||||
, dialog
|
, Dialog, MultiSelect, Select, selectButton
|
||||||
)
|
)
|
||||||
|
|
||||||
{-| This module contains functions for displaying data.
|
{-| This module contains functions for displaying data.
|
||||||
|
|
||||||
@docs select, multiSelect, collapsable, carousel, scrim, tab
|
@docs select, multiSelect, collapsable, carousel, modal, tab, dialog
|
||||||
|
|
||||||
|
|
||||||
# DEPRECATED
|
# DEPRECATED
|
||||||
@ -20,67 +20,97 @@ import Element.Background as Background
|
|||||||
import Element.Events as Events
|
import Element.Events as Events
|
||||||
import Element.Input as Input
|
import Element.Input as Input
|
||||||
import Set exposing (Set)
|
import Set exposing (Set)
|
||||||
|
import Widget.Button as Button exposing (Button, ButtonStyle, TextButton)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Select msg =
|
||||||
|
{ selected : Maybe Int
|
||||||
|
, options :
|
||||||
|
List
|
||||||
|
{ text : String
|
||||||
|
, icon : Element Never
|
||||||
|
}
|
||||||
|
, onSelect : Int -> Maybe msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias MultiSelect msg =
|
||||||
|
{ selected : Set Int
|
||||||
|
, options :
|
||||||
|
List
|
||||||
|
{ text : String
|
||||||
|
, icon : Element Never
|
||||||
|
}
|
||||||
|
, onSelect : Int -> Maybe msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Dialog msg =
|
||||||
|
{ title : Maybe String
|
||||||
|
, body : Element msg
|
||||||
|
, accept : Maybe (TextButton msg)
|
||||||
|
, dismiss : Maybe (TextButton msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A simple button
|
||||||
|
-}
|
||||||
|
selectButton :
|
||||||
|
ButtonStyle msg
|
||||||
|
-> ( Bool, Button msg )
|
||||||
|
-> Element msg
|
||||||
|
selectButton style ( selected, b ) =
|
||||||
|
b
|
||||||
|
|> Button.view
|
||||||
|
{ style
|
||||||
|
| container =
|
||||||
|
style.container
|
||||||
|
++ (if selected then
|
||||||
|
style.active
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| Selects one out of multiple options. This can be used for radio buttons or Menus.
|
{-| Selects one out of multiple options. This can be used for radio buttons or Menus.
|
||||||
-}
|
-}
|
||||||
select :
|
select :
|
||||||
{ selected : Maybe a
|
Select msg
|
||||||
, options : List a
|
-> List ( Bool, Button msg )
|
||||||
, label : a -> Element msg
|
select { selected, options, onSelect } =
|
||||||
, onChange : a -> msg
|
|
||||||
, attributes : Bool -> List (Attribute msg)
|
|
||||||
}
|
|
||||||
-> List (Element msg)
|
|
||||||
select { selected, options, label, onChange, attributes } =
|
|
||||||
options
|
options
|
||||||
|> List.map
|
|> List.indexedMap
|
||||||
(\a ->
|
(\i a ->
|
||||||
Input.button (attributes (selected == Just a))
|
( selected == Just i
|
||||||
{ onPress = a |> onChange |> Just
|
, { onPress = i |> onSelect
|
||||||
, label = label a
|
, text = a.text
|
||||||
}
|
, icon = a.icon
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
{-| Selects multible options. This can be used for checkboxes.
|
{-| Selects multible options. This can be used for checkboxes.
|
||||||
-}
|
-}
|
||||||
multiSelect :
|
multiSelect :
|
||||||
{ selected : Set comparable
|
MultiSelect msg
|
||||||
, options : List comparable
|
-> List ( Bool, Button msg )
|
||||||
, label : comparable -> Element msg
|
multiSelect { selected, options, onSelect } =
|
||||||
, onChange : comparable -> msg
|
|
||||||
, attributes : Bool -> List (Attribute msg)
|
|
||||||
}
|
|
||||||
-> List (Element msg)
|
|
||||||
multiSelect { selected, options, label, onChange, attributes } =
|
|
||||||
options
|
options
|
||||||
|> List.map
|
|> List.indexedMap
|
||||||
(\a ->
|
(\i a ->
|
||||||
Input.button (attributes (selected |> Set.member a))
|
( selected |> Set.member i
|
||||||
{ onPress = a |> onChange |> Just
|
, { onPress = i |> onSelect
|
||||||
, label =
|
, text = a.text
|
||||||
label a
|
, icon = a.icon
|
||||||
}
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
{-| Some collapsable content.
|
{-| 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 :
|
collapsable :
|
||||||
{ onToggle : Bool -> msg
|
{ onToggle : Bool -> msg
|
||||||
@ -107,81 +137,92 @@ collapsable { onToggle, isCollapsed, label, content } =
|
|||||||
{-| Displayes a list of contents in a tab
|
{-| Displayes a list of contents in a tab
|
||||||
-}
|
-}
|
||||||
tab :
|
tab :
|
||||||
List (Attribute msg)
|
{ style
|
||||||
->
|
| tabButton : ButtonStyle msg
|
||||||
{ selected : a
|
, tabRow : List (Attribute msg)
|
||||||
, options : List a
|
}
|
||||||
, onChange : a -> msg
|
-> Select msg
|
||||||
, label : a -> Element msg
|
-> (Maybe Int -> Element msg)
|
||||||
, content : a -> Element msg
|
|
||||||
, attributes : Bool -> List (Attribute msg)
|
|
||||||
}
|
|
||||||
-> Element msg
|
-> Element msg
|
||||||
tab atts { selected, options, onChange, label, content, attributes } =
|
tab style options content =
|
||||||
[ select
|
[ options
|
||||||
{ selected = Just selected
|
|> select
|
||||||
, options = options
|
|> List.map (selectButton style.tabButton)
|
||||||
, label = label
|
|> Element.row style.tabRow
|
||||||
, onChange = onChange
|
, options.selected
|
||||||
, attributes = attributes
|
|> content
|
||||||
}
|
|
||||||
|> Element.row atts
|
|
||||||
, content selected
|
|
||||||
]
|
]
|
||||||
|> Element.column []
|
|> Element.column []
|
||||||
|
|
||||||
|
|
||||||
{-| DEPRECATED. Use scrim instead.
|
|
||||||
-}
|
|
||||||
dialog :
|
dialog :
|
||||||
{ onDismiss : Maybe msg
|
{ containerColumn : List (Attribute msg)
|
||||||
, content : Element msg
|
, title : List (Attribute msg)
|
||||||
|
, buttonRow : List (Attribute msg)
|
||||||
|
, accept : ButtonStyle msg
|
||||||
|
, dismiss : ButtonStyle msg
|
||||||
}
|
}
|
||||||
-> Element msg
|
-> Dialog msg
|
||||||
dialog { onDismiss, content } =
|
-> { onDismiss : Maybe msg, content : Element msg }
|
||||||
content
|
dialog style { title, body, accept, dismiss } =
|
||||||
|> Element.el
|
{ onDismiss =
|
||||||
[ Element.centerX
|
case ( accept, dismiss ) of
|
||||||
, Element.centerY
|
( Nothing, Nothing ) ->
|
||||||
]
|
Nothing
|
||||||
|> Element.el
|
|
||||||
([ Element.width <| Element.fill
|
( Nothing, Just { onPress } ) ->
|
||||||
, Element.height <| Element.fill
|
onPress
|
||||||
, Background.color <| Element.rgba255 0 0 0 0.5
|
|
||||||
]
|
( Just _, _ ) ->
|
||||||
++ (onDismiss
|
Nothing
|
||||||
|> Maybe.map (Events.onClick >> List.singleton)
|
, content =
|
||||||
|> Maybe.withDefault []
|
Element.column
|
||||||
)
|
(style.containerColumn
|
||||||
|
++ [ Element.centerX
|
||||||
|
, Element.centerY
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
[ title
|
||||||
|
|> Maybe.map
|
||||||
|
(Element.text
|
||||||
|
>> Element.el style.title
|
||||||
|
)
|
||||||
|
|> Maybe.withDefault Element.none
|
||||||
|
, body
|
||||||
|
, Element.row
|
||||||
|
(style.buttonRow
|
||||||
|
++ [ Element.alignRight
|
||||||
|
, Element.width <| Element.shrink
|
||||||
|
]
|
||||||
|
)
|
||||||
|
(case ( accept, dismiss ) of
|
||||||
|
( Just acceptButton, Nothing ) ->
|
||||||
|
acceptButton
|
||||||
|
|> Button.viewTextOnly style.accept
|
||||||
|
|> List.singleton
|
||||||
|
|
||||||
|
( Just acceptButton, Just dismissButton ) ->
|
||||||
{-| A scrim to block the interaction with the site. Usefull for modals and side panels
|
[ dismissButton
|
||||||
|
|> Button.viewTextOnly style.dismiss
|
||||||
If the scrim is clicked a message may be send. Also one can place an element infront.
|
, acceptButton
|
||||||
|
|> Button.viewTextOnly style.accept
|
||||||
Framework.Layout
|
|
||||||
[ Wiget.scrim
|
|
||||||
{ onDismiss = Just <| ToggleDialog False
|
|
||||||
, content =
|
|
||||||
[ "This is a dialog window"
|
|
||||||
|> Element.text
|
|
||||||
, Input.button []
|
|
||||||
{onPress = Just <| ToggleDialog False
|
|
||||||
, label = Element.text "Ok"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|> Element.column
|
|
||||||
[ Element.centerX
|
|
||||||
, Element.centerY
|
|
||||||
]
|
]
|
||||||
}
|
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A modal.
|
||||||
|
|
||||||
|
NOTE: to stop the screen from scrolling, just set the height of the layout to the height of the screen.
|
||||||
|
|
||||||
-}
|
-}
|
||||||
scrim : { onDismiss : Maybe msg, content : Element msg } -> List (Attribute msg)
|
modal : { onDismiss : Maybe msg, content : Element msg } -> List (Attribute msg)
|
||||||
scrim { onDismiss, content } =
|
modal { onDismiss, content } =
|
||||||
Element.el
|
[ Element.el
|
||||||
([ Element.width <| Element.fill
|
([ Element.width <| Element.fill
|
||||||
, Element.height <| Element.fill
|
, Element.height <| Element.fill
|
||||||
, Background.color <| Element.rgba255 0 0 0 0.5
|
, Background.color <| Element.rgba255 0 0 0 0.5
|
||||||
@ -193,7 +234,8 @@ scrim { onDismiss, content } =
|
|||||||
)
|
)
|
||||||
content
|
content
|
||||||
|> Element.inFront
|
|> Element.inFront
|
||||||
|> List.singleton
|
, Element.clip
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
{-| A Carousel circles through a non empty list of contents.
|
{-| A Carousel circles through a non empty list of contents.
|
||||||
|
82
src/Widget/Button.elm
Normal file
82
src/Widget/Button.elm
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
module Widget.Button exposing
|
||||||
|
( Button
|
||||||
|
, ButtonStyle
|
||||||
|
, TextButton
|
||||||
|
, view
|
||||||
|
, viewIconOnly
|
||||||
|
, viewTextOnly
|
||||||
|
)
|
||||||
|
|
||||||
|
import Element exposing (Attribute, Element)
|
||||||
|
import Element.Input as Input
|
||||||
|
import Element.Region as Region
|
||||||
|
|
||||||
|
|
||||||
|
type alias Button msg =
|
||||||
|
{ text : String
|
||||||
|
, icon : Element Never
|
||||||
|
, onPress : Maybe msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias TextButton msg =
|
||||||
|
{ text : String
|
||||||
|
, onPress : Maybe msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias ButtonStyle msg =
|
||||||
|
{ container : List (Attribute msg)
|
||||||
|
, disabled : List (Attribute msg)
|
||||||
|
, label : List (Attribute msg)
|
||||||
|
, active : List (Attribute msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
viewIconOnly : ButtonStyle msg -> Button msg -> Element msg
|
||||||
|
viewIconOnly style { onPress, text, icon } =
|
||||||
|
Input.button
|
||||||
|
(style.container
|
||||||
|
++ (if onPress == Nothing then
|
||||||
|
style.disabled
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
++ [ Region.description text ]
|
||||||
|
)
|
||||||
|
{ onPress = onPress
|
||||||
|
, label =
|
||||||
|
icon |> Element.map never
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
viewTextOnly : ButtonStyle msg -> TextButton msg -> Element msg
|
||||||
|
viewTextOnly style { onPress, text } =
|
||||||
|
view style
|
||||||
|
{ onPress = onPress
|
||||||
|
, text = text
|
||||||
|
, icon = Element.none
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| The first argument can be used to define the spacing between the icon and the text
|
||||||
|
-}
|
||||||
|
view : ButtonStyle msg -> Button msg -> Element msg
|
||||||
|
view style { onPress, text, icon } =
|
||||||
|
Input.button
|
||||||
|
(style.container
|
||||||
|
++ (if onPress == Nothing then
|
||||||
|
style.disabled
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{ onPress = onPress
|
||||||
|
, label =
|
||||||
|
Element.row style.label
|
||||||
|
[ icon |> Element.map never
|
||||||
|
, Element.text text
|
||||||
|
]
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
module Widget.ScrollingNav exposing
|
module Widget.ScrollingNav exposing
|
||||||
( Model, init, view, viewSections, current
|
( Model, init, view, current
|
||||||
, jumpTo, syncPositions
|
, jumpTo, syncPositions
|
||||||
, getPos, jumpToWithOffset, setPos
|
, getPos, jumpToWithOffset, setPos, toSelect
|
||||||
)
|
)
|
||||||
|
|
||||||
{-| The Scrolling Nav is a navigation bar thats updates while you scroll through
|
{-| The Scrolling Nav is a navigation bar thats updates while you scroll through
|
||||||
@ -20,16 +20,18 @@ the page. Clicking on a navigation button will scroll directly to that section.
|
|||||||
-}
|
-}
|
||||||
|
|
||||||
import Browser.Dom as Dom
|
import Browser.Dom as Dom
|
||||||
import Element exposing (Attribute, Element)
|
import Element exposing (Element)
|
||||||
import Framework.Grid as Grid
|
import Framework.Grid as Grid
|
||||||
import Html.Attributes as Attributes
|
import Html.Attributes as Attributes
|
||||||
import IntDict exposing (IntDict)
|
import IntDict exposing (IntDict)
|
||||||
import Task exposing (Task)
|
import Task exposing (Task)
|
||||||
|
import Widget exposing (Select)
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
{-| -}
|
||||||
type alias Model section =
|
type alias Model section =
|
||||||
{ labels : section -> String
|
{ toString : section -> String
|
||||||
|
, fromString : String -> Maybe section
|
||||||
, positions : IntDict String
|
, positions : IntDict String
|
||||||
, arrangement : List section
|
, arrangement : List section
|
||||||
, scrollPos : Int
|
, scrollPos : Int
|
||||||
@ -39,13 +41,15 @@ type alias Model section =
|
|||||||
{-| The intial state include the labels and the arrangement of the sections
|
{-| The intial state include the labels and the arrangement of the sections
|
||||||
-}
|
-}
|
||||||
init :
|
init :
|
||||||
{ labels : section -> String
|
{ toString : section -> String
|
||||||
|
, fromString : String -> Maybe section
|
||||||
, arrangement : List section
|
, arrangement : List section
|
||||||
, toMsg : Result Dom.Error (Model section -> Model section) -> msg
|
, toMsg : Result Dom.Error (Model section -> Model section) -> msg
|
||||||
}
|
}
|
||||||
-> ( Model section, Cmd msg )
|
-> ( Model section, Cmd msg )
|
||||||
init { labels, arrangement, toMsg } =
|
init { toString, fromString, arrangement, toMsg } =
|
||||||
{ labels = labels
|
{ toString = toString
|
||||||
|
, fromString = fromString
|
||||||
, positions = IntDict.empty
|
, positions = IntDict.empty
|
||||||
, arrangement = arrangement
|
, arrangement = arrangement
|
||||||
, scrollPos = 0
|
, scrollPos = 0
|
||||||
@ -82,8 +86,8 @@ jumpTo :
|
|||||||
}
|
}
|
||||||
-> Model section
|
-> Model section
|
||||||
-> Cmd msg
|
-> Cmd msg
|
||||||
jumpTo { section, onChange } { labels } =
|
jumpTo { section, onChange } { toString } =
|
||||||
Dom.getElement (section |> labels)
|
Dom.getElement (section |> toString)
|
||||||
|> Task.andThen
|
|> Task.andThen
|
||||||
(\{ element } ->
|
(\{ element } ->
|
||||||
Dom.setViewport 0 element.y
|
Dom.setViewport 0 element.y
|
||||||
@ -100,8 +104,8 @@ jumpToWithOffset :
|
|||||||
}
|
}
|
||||||
-> Model section
|
-> Model section
|
||||||
-> Cmd msg
|
-> Cmd msg
|
||||||
jumpToWithOffset { offset, section, onChange } { labels } =
|
jumpToWithOffset { offset, section, onChange } { toString } =
|
||||||
Dom.getElement (section |> labels)
|
Dom.getElement (section |> toString)
|
||||||
|> Task.andThen
|
|> Task.andThen
|
||||||
(\{ element } ->
|
(\{ element } ->
|
||||||
Dom.setViewport 0 (element.y - offset)
|
Dom.setViewport 0 (element.y - offset)
|
||||||
@ -111,11 +115,11 @@ jumpToWithOffset { offset, section, onChange } { labels } =
|
|||||||
|
|
||||||
{-| -}
|
{-| -}
|
||||||
syncPositions : Model section -> Task Dom.Error (Model section -> Model section)
|
syncPositions : Model section -> Task Dom.Error (Model section -> Model section)
|
||||||
syncPositions { labels, arrangement } =
|
syncPositions { toString, arrangement } =
|
||||||
arrangement
|
arrangement
|
||||||
|> List.map
|
|> List.map
|
||||||
(\label ->
|
(\label ->
|
||||||
Dom.getElement (labels label)
|
Dom.getElement (toString label)
|
||||||
|> Task.map
|
|> Task.map
|
||||||
(\x ->
|
(\x ->
|
||||||
( x.element.y |> round
|
( x.element.y |> round
|
||||||
@ -133,7 +137,7 @@ syncPositions { labels, arrangement } =
|
|||||||
| positions =
|
| positions =
|
||||||
model.positions
|
model.positions
|
||||||
|> IntDict.insert pos
|
|> IntDict.insert pos
|
||||||
(label |> model.labels)
|
(label |> model.toString)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
m
|
m
|
||||||
@ -151,27 +155,29 @@ current fromString { positions, scrollPos } =
|
|||||||
|> Maybe.andThen fromString
|
|> Maybe.andThen fromString
|
||||||
|
|
||||||
|
|
||||||
{-| -}
|
toSelect : (Int -> Maybe msg) -> Model section -> Select msg
|
||||||
viewSections :
|
toSelect onSelect ({ arrangement, toString, fromString } as model) =
|
||||||
{ label : String -> Element msg
|
{ selected =
|
||||||
, fromString : String -> Maybe section
|
arrangement
|
||||||
, onSelect : section -> msg
|
|> List.indexedMap (\i s -> ( i, s ))
|
||||||
, attributes : Bool -> List (Attribute msg)
|
|> List.filterMap
|
||||||
}
|
(\( i, s ) ->
|
||||||
-> Model section
|
if Just s == current fromString model then
|
||||||
->
|
Just i
|
||||||
{ selected : Maybe section
|
|
||||||
, options : List section
|
else
|
||||||
, label : section -> Element msg
|
Nothing
|
||||||
, onChange : section -> msg
|
)
|
||||||
, attributes : Bool -> List (Attribute msg)
|
|> List.head
|
||||||
}
|
, options =
|
||||||
viewSections { label, fromString, onSelect, attributes } ({ arrangement, labels } as model) =
|
arrangement
|
||||||
{ selected = model |> current fromString
|
|> List.map
|
||||||
, options = arrangement
|
(\s ->
|
||||||
, label = \elem -> label (elem |> labels)
|
{ text = toString s
|
||||||
, onChange = onSelect
|
, icon = Element.none
|
||||||
, attributes = attributes
|
}
|
||||||
|
)
|
||||||
|
, onSelect = onSelect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -180,13 +186,13 @@ view :
|
|||||||
(section -> Element msg)
|
(section -> Element msg)
|
||||||
-> Model section
|
-> Model section
|
||||||
-> Element msg
|
-> Element msg
|
||||||
view asElement { labels, arrangement } =
|
view asElement { toString, arrangement } =
|
||||||
arrangement
|
arrangement
|
||||||
|> List.map
|
|> List.map
|
||||||
(\header ->
|
(\header ->
|
||||||
Element.el
|
Element.el
|
||||||
[ header
|
[ header
|
||||||
|> labels
|
|> toString
|
||||||
|> Attributes.id
|
|> Attributes.id
|
||||||
|> Element.htmlAttribute
|
|> Element.htmlAttribute
|
||||||
, Element.width <| Element.fill
|
, Element.width <| Element.fill
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
module Widget.Snackbar exposing
|
module Widget.Snackbar exposing
|
||||||
( Model, init, current, timePassed
|
( Model, init, current, timePassed
|
||||||
, insert, insertFor, dismiss
|
, insert, insertFor, dismiss
|
||||||
|
, Message, view
|
||||||
)
|
)
|
||||||
|
|
||||||
{-| A [snackbar](https://material.io/components/snackbars/) shows notification, one at a time.
|
{-| A [snackbar](https://material.io/components/snackbars/) shows notification, one at a time.
|
||||||
@ -17,7 +18,16 @@ module Widget.Snackbar exposing
|
|||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
import Element exposing (Attribute, Element)
|
||||||
import Queue exposing (Queue)
|
import Queue exposing (Queue)
|
||||||
|
import Widget
|
||||||
|
import Widget.Button as Button exposing (ButtonStyle, TextButton)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Message msg =
|
||||||
|
{ text : String
|
||||||
|
, button : Maybe (TextButton msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| A snackbar has a queue of Notifications, each with the amount of ms the message should be displayed
|
{-| A snackbar has a queue of Notifications, each with the amount of ms the message should be displayed
|
||||||
@ -92,3 +102,31 @@ timePassed ms model =
|
|||||||
current : Model a -> Maybe a
|
current : Model a -> Maybe a
|
||||||
current model =
|
current model =
|
||||||
model.current |> Maybe.map Tuple.first
|
model.current |> Maybe.map Tuple.first
|
||||||
|
|
||||||
|
|
||||||
|
view :
|
||||||
|
{ row : List (Attribute msg)
|
||||||
|
, text : List (Attribute msg)
|
||||||
|
, button : ButtonStyle msg
|
||||||
|
}
|
||||||
|
-> (a -> Message msg)
|
||||||
|
-> Model a
|
||||||
|
-> Maybe (Element msg)
|
||||||
|
view style toMessage model =
|
||||||
|
model
|
||||||
|
|> current
|
||||||
|
|> Maybe.map
|
||||||
|
(toMessage
|
||||||
|
>> (\{ text, button } ->
|
||||||
|
[ text
|
||||||
|
|> Element.text
|
||||||
|
|> List.singleton
|
||||||
|
|> Element.paragraph style.text
|
||||||
|
, button
|
||||||
|
|> Maybe.map
|
||||||
|
(Button.viewTextOnly style.button)
|
||||||
|
|> Maybe.withDefault Element.none
|
||||||
|
]
|
||||||
|
|> Element.row style.row
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user