diff --git a/elm.json b/elm.json index e347336..0b8bf4e 100644 --- a/elm.json +++ b/elm.json @@ -18,6 +18,7 @@ "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/svg": "1.0.1 <= 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", @@ -28,4 +29,4 @@ "test-dependencies": { "elm-explorations/test": "1.2.1 <= v < 2.0.0" } -} \ No newline at end of file +} diff --git a/example/src/Component.elm b/example/src/Component.elm index 952beea..ea6dab8 100644 --- a/example/src/Component.elm +++ b/example/src/Component.elm @@ -91,10 +91,9 @@ update msg model = ) -filterSelect : FilterSelect.Model -> Element Msg +filterSelect : FilterSelect.Model -> (String,Element Msg) filterSelect model = - Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) <| - [ Element.el Heading.h3 <| Element.text "Filter Select" + ( "Filter Select" , case model.selected of Just selected -> Element.row Grid.compact @@ -128,13 +127,12 @@ filterSelect model = ) |> Element.wrappedRow [ Element.spacing 10 ] ] - ] + ) -validatedInput : ValidatedInput.Model () ( String, String ) -> Element Msg +validatedInput : ValidatedInput.Model () ( String, String ) -> (String,Element Msg) validatedInput model = - Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) <| - [ Element.el Heading.h3 <| Element.text "Validated Input" + ( "Validated Input" , ValidatedInput.view Input.simple model { label = "First Name, Sir Name" @@ -152,19 +150,19 @@ validatedInput model = |> Element.el (Tag.simple ++ Group.right ++ Color.primary) ] } - ] + ) -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.height <| Element.shrink]) <| - [ filterSelect model.filterSelect - , validatedInput model.validatedInput - - ] +view : (Msg -> msg) -> Model -> + { title : String + , description : String + , items : List (String,Element msg) + } +view msgMapper model = + { title = "Components" + , description = "Components have a Model, an Update- and sometimes even a Subscription-function. It takes some time to set them up correctly." + , items = + [ filterSelect model.filterSelect + , validatedInput model.validatedInput ] + |> List.map (Tuple.mapSecond (Element.map msgMapper) ) + } diff --git a/example/src/Example.elm b/example/src/Example.elm index 9b58e91..332b723 100644 --- a/example/src/Example.elm +++ b/example/src/Example.elm @@ -5,7 +5,7 @@ import Browser.Dom as Dom exposing (Viewport) import Browser.Events as Events import Browser.Navigation as Navigation import Component -import Element exposing (DeviceClass(..), Element) +import Element exposing (DeviceClass(..), Element,Attribute) import Element.Input as Input import Element.Font as Font import Element.Border as Border @@ -29,21 +29,27 @@ import Stateless import Task import Time import Widget +import Widget.Button exposing (ButtonStyle) import Widget.FilterSelect as FilterSelect import Widget.ScrollingNav as ScrollingNav import Widget.Snackbar as Snackbar import Widget.ValidatedInput as ValidatedInput import Data.Section as Section exposing (Section(..)) +import Array type alias LoadedModel = { component : Component.Model , stateless : Stateless.Model , reusable : Reusable.Model , scrollingNav : ScrollingNav.Model Section - , layout : Layout + , layout : Layout LoadedMsg , displayDialog : Bool - , deviceClass : DeviceClass - , search : String + , window : { height : Int, width : Int } + , search : + { raw : String + , current : String + , remaining : Int + } } @@ -58,7 +64,7 @@ type LoadedMsg | ComponentSpecific Component.Msg | UpdateScrollingNav (ScrollingNav.Model Section -> ScrollingNav.Model Section) | TimePassed Int - | AddSnackbar String + | AddSnackbar (String,Bool) | ToggleDialog Bool | ChangedSidebar (Maybe Part) | Resized { width : Int, height : Int } @@ -72,9 +78,64 @@ type Msg = LoadedSpecific LoadedMsg | 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 = - { 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 , header = Framework.container @@ -83,28 +144,42 @@ style = , Element.height <| Element.px <| 42 ] , menuButton = - Button.simple ++ Group.center ++ Color.dark - , menuButtonSelected = - Color.primary + { label = Grid.simple + , container = Button.simple ++ Group.center ++ Color.dark + , disabled = Color.disabled + , active = Color.primary + } , sheetButton = - Button.fill - ++ Group.center - ++ Color.light - ++ [Font.alignLeft] - , sheetButtonSelected = - Color.primary - , tabButton = - [ Element.height <| Element.px <| 42 - , Border.widthEach - { top = 0, - left = 0, - right = 0, - bottom = 8 - } - ] - , tabButtonSelected = - [ Border.color Color.turquoise - ] + { container = + Button.fill + ++ Group.center + ++ Color.light + ++ [Font.alignLeft] + , label = Grid.simple + , disabled = Color.disabled + , active = Color.primary + } + , menuTabButton = + { container = + [ Element.height <| Element.px <| 42 + , Border.widthEach + { top = 0, + left = 0, + right = 0, + 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 = Color.light ++ [ Element.width <| Element.maximum 256 <| Element.fill] , menuIcon = @@ -116,7 +191,16 @@ style = , searchIcon = Icons.search |> Element.html |> Element.el [] , 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 ( scrollingNav, cmd ) = ScrollingNav.init - { labels = Section.toString + { toString = Section.toString + , fromString = Section.fromString , arrangement = Section.asList , toMsg = \result -> case result of @@ -141,13 +226,15 @@ initialModel { viewport } = , scrollingNav = scrollingNav , layout = Layout.init , displayDialog = False - , deviceClass = + , window = { width = viewport.width |> round , height = viewport.height |> round } - |> Element.classifyDevice - |> .class - , search = "" + , search = + { raw = "" + , current = "" + , remaining = 0 + } } , cmd ) @@ -171,25 +258,23 @@ view model = Layout.view [] { dialog = if m.displayDialog then - Just - { onDismiss = Just <| ToggleDialog False - , content = - [ Element.el Heading.h3 <| Element.text "Dialog" - , "This is a dialog window" + { body = + "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 - ++ [Element.centerX, Element.centerY] - ) + , title = Just "Dialog" + , accept = Just + { text = "Ok" + , onPress = Just <| ToggleDialog False } + , dismiss = Just + { text = "Dismiss" + , onPress = Just <| ToggleDialog False + } + } + |> Widget.dialog style.dialog + |> Just else Nothing @@ -198,11 +283,12 @@ view model = , [ m.scrollingNav |> ScrollingNav.view (\section -> - case section of + ( case section of ComponentViews -> m.component - |> Component.view - |> Element.map ComponentSpecific + |> Component.view ComponentSpecific + + ReusableViews -> Reusable.view @@ -218,6 +304,39 @@ view model = , changedSheet = ChangedSidebar } 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 @@ -225,52 +344,35 @@ view model = |> Element.column Grid.compact , style = style , layout = m.layout - , deviceClass = m.deviceClass + , window = m.window , menu = - { selected = - Section.asList - |> List.indexedMap (\i s -> (i,s)) - |> List.filterMap - ( \(i,s) -> - if m.scrollingNav - |> ScrollingNav.current Section.fromString - |> (==) (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 - } - ) - } + m.scrollingNav + |> ScrollingNav.toSelect + (\int -> + m.scrollingNav.arrangement + |> Array.fromList + |> Array.get int + |> Maybe.map JumpTo + ) , actions = [ { 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 [] } , { onPress = Just <| Load "https://github.com/Orasund/elm-ui-widgets" - , label = "Github" + , text = "Github" , icon = Icons.github|> Element.html |> Element.el [] } , { onPress = Nothing - , label = "Placeholder" + , text = "Placeholder" , icon = Icons.circle|> Element.html |> Element.el [] } , { onPress = Nothing - , label = "Placeholder" + , text = "Placeholder" , icon = Icons.triangle|> Element.html |> Element.el [] } , { onPress = Nothing - , label = "Placeholder" + , text = "Placeholder" , icon = Icons.square|> Element.html |> Element.el [] } ] @@ -281,7 +383,7 @@ view model = |> Element.el Heading.h1 , search = Just - { text = m.search + { text = m.search.raw , onChange = ChangedSearch , label = "Search" } @@ -330,15 +432,44 @@ updateLoaded msg model = ) TimePassed int -> + let + search = model.search + in ( { model | 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 |> Task.perform UpdateScrollingNav ) - AddSnackbar string -> - ( { model | layout = model.layout |> Layout.queueMessage string } + AddSnackbar (string,bool) -> + ( { 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 ) @@ -347,8 +478,8 @@ updateLoaded msg model = , Cmd.none ) - Resized screen -> - ( { model | deviceClass = screen |> Element.classifyDevice |> .class } + Resized window -> + ( { model | window = window } , Cmd.none ) @@ -370,7 +501,17 @@ updateLoaded msg model = ) ChangedSearch string -> - ( { model | search = string},Cmd.none) + let + search = model.search + in + ( { model | search = + { search + | raw = string + , remaining = 300 + } + } + , Cmd.none + ) Idle -> ( model , Cmd.none) diff --git a/example/src/Icons.elm b/example/src/Icons.elm index c6b19d1..1dc7422 100644 --- a/example/src/Icons.elm +++ b/example/src/Icons.elm @@ -7,6 +7,7 @@ module Icons exposing , triangle , square , search + , slash ) import Html exposing (Html) @@ -66,6 +67,13 @@ circle = [ 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 = svgFeatherIcon "triangle" diff --git a/example/src/Reusable.elm b/example/src/Reusable.elm index 8bf0ab5..c911b18 100644 --- a/example/src/Reusable.elm +++ b/example/src/Reusable.elm @@ -54,24 +54,37 @@ init = SortTable.sortBy { title = "Name", asc = True } -snackbar : (String -> msg) -> Element msg +snackbar : ((String,Bool) -> msg) -> (String,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." + ( "Snackbar" + , [Input.button Button.simple + { onPress = Just <| addSnackbar <| + ("This is a notification. It will disappear after 10 seconds." + , False + ) , label = "Add Notification" |> Element.text |> List.singleton |> Element.paragraph [] } - ] - |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) + , Input.button Button.simple + { 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 -> Element Msg +sortTable : SortTable.Model -> (String,Element Msg) sortTable model = - [ Element.el Heading.h3 <| Element.text "Sort Table" + ( "Sort Table" , SortTable.view { content = [ { id = 1, name = "Antonio", rating = 2.456 } @@ -139,35 +152,31 @@ sortTable model = } ) |> Element.table Grid.simple - ] - |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) + ) -scrollingNavCard : Element msg +scrollingNavCard : (String , Element msg ) 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." |> List.singleton |> Element.paragraph [] - ] - |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) - + ) view : - { addSnackbar : String -> msg + { addSnackbar : (String,Bool) -> msg , msgMapper : Msg -> msg , model : Model } - -> Element msg + -> { title : String + , description : String + , items : List (String,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.height <| Element.shrink]) <| - [ snackbar addSnackbar - , sortTable model |> Element.map msgMapper - , scrollingNavCard - ] + { title = "Reusable Views" + , description = "Reusable views have an internal state but no update function. You will need to do some wiring, but nothing complicated." + , items = + [ snackbar addSnackbar + , sortTable model |> Tuple.mapSecond (Element.map msgMapper) + , scrollingNavCard ] + } diff --git a/example/src/Stateless.elm b/example/src/Stateless.elm index 528eb1f..d946a8c 100644 --- a/example/src/Stateless.elm +++ b/example/src/Stateless.elm @@ -17,16 +17,34 @@ import Heroicons.Solid as Heroicons import Html exposing (Html) import Html.Attributes as Attributes import Set exposing (Set) -import Widget +import Widget.Button as Button exposing (ButtonStyle) 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 = { selected : Maybe Int , multiSelected : Set Int , isCollapsed : Bool , carousel : Int - , tab : Int + , tab : Maybe Int + , button : Bool } @@ -36,6 +54,7 @@ type Msg | ToggleCollapsable Bool | ChangedTab Int | SetCarousel Int + | ToggleButton Bool init : Model @@ -44,7 +63,8 @@ init = , multiSelected = Set.empty , isCollapsed = False , carousel = 0 - , tab = 1 + , tab = Just 1 + , button = True } @@ -91,35 +111,31 @@ update msg model = ) 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 = - [ Element.el Heading.h3 <| Element.text "Select" - , Widget.select - { selected = model.selected - , options = [ 1, 2, 42 ] - , label = String.fromInt >> Element.text - , onChange = ChangedSelected - , attributes = - \selected -> - Button.simple - ++ [ Border.width 0 - , Border.rounded 0 - ] - ++ (if selected then - Color.primary - - else - [] - ) - } + ( "Select" + , { selected = model.selected + , options = + [ 1, 2, 42 ] + |> List.map (\int -> + { text = String.fromInt int + , icon = Element.none + } + ) + , onSelect = ChangedSelected >> Just + } + |> Widget.select |> List.indexedMap (\i -> - Element.el - (Button.simple - ++ [ Element.padding <| 0 ] + Widget.selectButton + { buttonStyle + | container = buttonStyle.container ++ (if i == 0 then Group.left @@ -129,39 +145,31 @@ select model = else Group.center ) - ) + } ) + |> 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 = - [ Element.el Heading.h3 <| Element.text "Multi Select" - , Widget.multiSelect - { selected = model.multiSelected - , options = [ 1, 2, 42 ] - , label = String.fromInt >> Element.text - , onChange = ChangedMultiSelected - , attributes = - \selected -> - Button.simple - ++ [ Border.width 0 - , Border.rounded 0 - ] - ++ (if selected then - Color.primary - - else - [] - ) - } + ( "Multi Select" + , { selected = model.multiSelected + , options = + [ 1, 2, 42 ] + |> List.map (\int -> + { text = String.fromInt int + , icon = Element.none + }) + , onSelect = ChangedMultiSelected >> Just + } + |> Widget.multiSelect |> List.indexedMap (\i -> - Element.el - (Button.simple - ++ [ Element.padding <| 0 ] + Widget.selectButton + { buttonStyle + | container = buttonStyle.container ++ (if i == 0 then Group.left @@ -171,16 +179,14 @@ multiSelect model = else Group.center ) - ) + } ) |> Element.row Grid.compact - ] - |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) + ) - -collapsable : Model -> Element Msg +collapsable : Model -> (String,Element Msg) collapsable model = - [ Element.el Heading.h3 <| Element.text "Collapsable" + ( "Collapsable" , Widget.collapsable { onToggle = ToggleCollapsable , isCollapsed = model.isCollapsed @@ -196,28 +202,33 @@ collapsable model = ] , content = Element.text <| "Hello World" } - ] - |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) + ) - -tab : Model -> Element Msg +tab : Model -> (String,Element Msg) tab model = - [ Element.el Heading.h3 <| Element.text "Tab" - , Widget.tab Grid.simple - { selected = model.tab - , options = [ 1, 2, 3 ] - , onChange = ChangedTab - , label = \int -> "Tab " ++ String.fromInt int |> Element.text - , content = - \selected -> + ( "Tab" + , Widget.tab + { tabButton = tabButtonStyle + , tabRow = Grid.simple + } + { selected = model.tab + , options = [ 1, 2, 3 ] + |> List.map (\int -> + { text = "Tab " ++ (int |> String.fromInt) + , icon = Element.none + } + ) + , onSelect = ChangedTab >> Just + } <| + (\selected -> (case selected of - 1 -> + Just 0 -> "This is Tab 1" - 2 -> + Just 1 -> "This is the second tab" - 3 -> + Just 2 -> "The thrid and last tab" _ -> @@ -225,46 +236,35 @@ tab model = ) |> Element.text |> Element.el (Card.small ++ Group.bottom) - , attributes = - \selected -> - Button.simple - ++ Group.top - ++ (if selected then - Color.primary + ) + ) - else - [] - ) - } - ] - |> Element.column (Grid.simple ++ Card.large ++ [Element.height <| Element.fill]) - - -scrim : - { showDialog : msg - , changedSheet : Maybe Part -> msg - } -> Model -> Element msg -scrim {showDialog,changedSheet} model = - [ Element.el Heading.h3 <| Element.text "Scrim" +modal : (Maybe Part -> msg) -> Model -> (String,Element msg) +modal changedSheet model = + ( "Modal" + , [ 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 + ) + +dialog : msg -> Model -> (String,Element msg) +dialog showDialog model = + ( "Dialog" , Input.button Button.simple { onPress = Just showDialog , 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 -> Element Msg +carousel : Model -> (String,Element Msg) carousel model = - [ Element.el Heading.h3 <| Element.text "Carousel" + ( "Carousel" , Widget.carousel { content = ( Color.cyan, [ Color.yellow, Color.green, Color.red ] |> Array.fromList ) , current = model.carousel @@ -294,33 +294,47 @@ carousel model = ] |> 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 : { msgMapper : Msg -> msg , showDialog : msg , changedSheet : Maybe Part -> msg - } -> Model -> Element msg + } -> Model + -> { title : String + , description : String + , items : List (String,Element msg) + } view { msgMapper, showDialog, changedSheet } 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 ++ [Element.height <| Element.shrink]) - <| - [ select model |> Element.map msgMapper - , multiSelect model |> Element.map msgMapper - , collapsable model |> Element.map msgMapper - , scrim - { showDialog = showDialog - , changedSheet = changedSheet - } model - , carousel model |> Element.map msgMapper - , tab model |> Element.map msgMapper - ] + { title = "Stateless Views" + , description = "Stateless views are simple functions that view some content. No wiring required." + , items = + [ iconButton model |> Tuple.mapSecond (Element.map msgMapper) + , select model |> Tuple.mapSecond (Element.map msgMapper) + , multiSelect model |> Tuple.mapSecond (Element.map msgMapper) + , collapsable model |> Tuple.mapSecond (Element.map msgMapper) + , modal changedSheet model + , carousel model |> Tuple.mapSecond (Element.map msgMapper) + , tab model |> Tuple.mapSecond (Element.map msgMapper) + , dialog showDialog model ] + } diff --git a/src/Core/Style.elm b/src/Core/Style.elm index 3d1d838..273fe65 100644 --- a/src/Core/Style.elm +++ b/src/Core/Style.elm @@ -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.Input as Input import Html exposing (Html) +import Widget +import Widget.Button as Button exposing (Button, ButtonStyle) -type alias Style msg = - { snackbar : List (Attribute msg) - , layout : List (Attribute msg) -> Element msg -> Html msg - , header : List (Attribute msg) - , sheet : List (Attribute msg) - , menuButton : List (Attribute msg) - , menuButtonSelected : List (Attribute msg) - , sheetButton : List (Attribute msg) - , sheetButtonSelected : List (Attribute msg) - , tabButton : List (Attribute msg) - , tabButtonSelected : List (Attribute msg) - , menuIcon : Element Never - , moreVerticalIcon : Element Never - , spacing : Int - , title : List (Attribute msg) - , searchIcon : Element Never - , search : List (Attribute msg) +type alias Style style msg = + { style + | snackbar : + { row : List (Attribute msg) + , text : List (Attribute msg) + , button : ButtonStyle msg + } + , layout : List (Attribute msg) -> Element msg -> Html msg + , header : List (Attribute msg) + , sheet : List (Attribute msg) + , sheetButton : ButtonStyle msg + , menuButton : ButtonStyle msg + , menuTabButton : ButtonStyle msg + , menuIcon : Element Never + , moreVerticalIcon : Element Never + , spacing : Int + , title : List (Attribute msg) + , searchIcon : Element Never + , search : List (Attribute msg) + , searchFill : List (Attribute msg) } -type alias ButtonInfo msg = - { label : String - , icon : Element Never - , onPress : Maybe msg - } +menuButton : Style style msg -> ( Bool, Button msg ) -> Element msg +menuButton style = + Widget.selectButton style.menuButton -menuButtonSelected : Style msg -> ButtonInfo msg -> Element msg -menuButtonSelected config { label, icon, onPress } = - Input.button (config.menuButton ++ config.menuButtonSelected) - { onPress = onPress - , label = - Element.row [ Element.spacing 8 ] - [ icon |> Element.map never - , Element.text label - ] - } +menuIconButton : Style style msg -> Button msg -> Element msg +menuIconButton style = + Button.viewIconOnly style.menuButton -menuButton : Style msg -> ButtonInfo msg -> Element msg -menuButton config { label, icon, onPress } = - Input.button config.menuButton - { onPress = onPress - , label = - Element.row [ Element.spacing 8 ] - [ icon |> Element.map never - , Element.text label - ] - } +sheetButton : Style style msg -> ( Bool, Button msg ) -> Element msg +sheetButton style = + Widget.selectButton style.sheetButton -menuIconButton : Style msg -> ButtonInfo msg -> Element msg -menuIconButton config { label, icon, onPress } = - Input.button config.menuButton - { 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 - ] - } +menuTabButton : Style style msg -> ( Bool, Button msg ) -> Element msg +menuTabButton style = + Widget.selectButton style.menuTabButton diff --git a/src/Layout.elm b/src/Layout.elm index ca0689f..4af1877 100644 --- a/src/Layout.elm +++ b/src/Layout.elm @@ -1,16 +1,13 @@ module Layout exposing (Layout, Part(..), activate, init, queueMessage, timePassed, view) import Array -import Browser.Dom exposing (Viewport) import Core.Style as Style exposing (Style) import Element exposing (Attribute, DeviceClass(..), Element) -import Element.Background as Background -import Element.Events as Events import Element.Input as Input import Html exposing (Html) -import Html.Attributes as Attributes -import Widget -import Widget.Snackbar as Snackbar +import Widget exposing (Select) +import Widget.Button as Button exposing (Button) +import Widget.Snackbar as Snackbar exposing (Message) type Part @@ -19,34 +16,34 @@ type Part | Search -type alias Layout = - { snackbar : Snackbar.Model String +type alias Layout msg = + { snackbar : Snackbar.Model (Message msg) , active : Maybe Part } -init : Layout +init : Layout msg init = { snackbar = Snackbar.init , active = Nothing } -queueMessage : String -> Layout -> Layout +queueMessage : Message msg -> Layout msg -> Layout msg queueMessage message layout = { layout | snackbar = layout.snackbar |> Snackbar.insert message } -activate : Maybe Part -> Layout -> Layout +activate : Maybe Part -> Layout msg -> Layout msg activate part layout = { layout | active = part } -timePassed : Int -> Layout -> Layout +timePassed : Int -> Layout msg -> Layout msg timePassed sec layout = case layout.active of Just LeftSheet -> @@ -64,28 +61,31 @@ timePassed sec layout = view : List (Attribute msg) -> - { deviceClass : DeviceClass + { window : { height : Int, width : Int } , dialog : Maybe { onDismiss : Maybe msg, content : Element msg } , content : Element msg - , layout : Layout + , layout : Layout msg , title : Element msg - , menu : - { selected : Int - , items : List { label : String, icon : Element Never, onPress : Maybe msg } - } + , menu : Select msg , search : Maybe { onChange : String -> msg , text : String , label : String } - , actions : List { label : String, icon : Element Never, onPress : Maybe msg } + , actions : List (Button msg) , onChangedSidebar : Maybe Part -> msg - , style : Style msg + , style : Style style 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 + deviceClass : DeviceClass + deviceClass = + window + |> Element.classifyDevice + |> .class + ( primaryActions, moreActions ) = ( if (actions |> List.length) > 4 then actions |> List.take 2 @@ -115,30 +115,29 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d [ (if (deviceClass == Phone) || (deviceClass == Tablet) - || ((menu.items |> List.length) > 5) + || ((menu.options |> List.length) > 5) then - [ Input.button style.menuButton + [ Button.viewIconOnly style.menuButton { onPress = Just <| onChangedSidebar <| Just LeftSheet - , label = style.menuIcon |> Element.map never + , icon = style.menuIcon |> Element.map never + , text = "Menu" } - , menu.items - |> Array.fromList - |> Array.get menu.selected - |> Maybe.map (.label >> Element.text >> Element.el style.title) + , menu.selected + |> Maybe.andThen + (\option -> + menu.options + |> Array.fromList + |> Array.get option + ) + |> Maybe.map (.text >> Element.text >> Element.el style.title) |> Maybe.withDefault title ] else [ title - , menu.items - |> List.indexedMap - (\i -> - if i == menu.selected then - Style.menuTabButtonSelected style - - else - Style.menuTabButton style - ) + , menu + |> Widget.select + |> List.map (Style.menuTabButton style) |> Element.row [ Element.width <| Element.shrink ] @@ -146,9 +145,9 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d ) |> Element.row [ Element.width <| Element.shrink - , Element.spacing 8 + , Element.spacing style.spacing ] - , if deviceClass == Phone then + , if deviceClass == Phone || deviceClass == Tablet then Element.none else @@ -166,37 +165,45 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d } ) |> Maybe.withDefault Element.none - , [ if deviceClass == Phone then - search - |> Maybe.map - (\{ label } -> - [ Style.menuButton style + , [ search + |> Maybe.map + (\{ label } -> + if deviceClass == Tablet then + [ Button.view style.menuButton { onPress = Just <| onChangedSidebar <| Just Search , 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 |> List.map (if deviceClass == Phone then Style.menuIconButton style else - Style.menuButton style + Button.view style.menuButton ) , if moreActions |> List.isEmpty then [] else - [ Style.menuButton style + [ Button.viewIconOnly style.menuButton { onPress = Just <| onChangedSidebar <| Just RightSheet , icon = style.moreVerticalIcon - , label = "" + , text = "More" } ] ] @@ -210,7 +217,7 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d (style.header ++ [ Element.padding 0 , Element.centerX - , Element.spaceEvenly + , Element.spacing style.spacing , Element.alignTop , Element.width <| Element.fill ] @@ -218,16 +225,13 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d snackbar = layout.snackbar - |> Snackbar.current + |> Snackbar.view style.snackbar identity |> Maybe.map - (Element.text - >> List.singleton - >> Element.paragraph style.snackbar - >> Element.el - [ Element.padding 8 - , Element.alignBottom - , Element.alignRight - ] + (Element.el + [ Element.padding style.spacing + , Element.alignBottom + , Element.alignRight + ] ) |> Maybe.withDefault Element.none @@ -236,15 +240,9 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d Just LeftSheet -> [ [ title ] - , menu.items - |> List.indexedMap - (\i -> - if i == menu.selected then - Style.sheetButtonSelected style - - else - Style.sheetButton style - ) + , menu + |> Widget.select + |> List.map (Style.sheetButton style) ] |> List.concat |> Element.column [ Element.width <| Element.fill ] @@ -257,7 +255,7 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d Just RightSheet -> moreActions - |> List.map (Style.sheetButton style) + |> List.map (Button.view style.sheetButton) |> Element.column [ Element.width <| Element.fill ] |> Element.el (style.sheet @@ -269,7 +267,11 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d Just Search -> case search of Just { onChange, text, label } -> - Input.text style.search + Input.text + (style.searchFill + ++ [ Element.width <| Element.fill + ] + ) { onChange = onChange , text = text , placeholder = @@ -297,33 +299,22 @@ view attributes { search, title, onChangedSidebar, menu, actions, deviceClass, d , Element.inFront snackbar ] , if (layout.active /= Nothing) || (dialog /= Nothing) then - Widget.scrim - { onDismiss = - Just <| - case dialog of - Just { onDismiss } -> - onDismiss - |> Maybe.withDefault - (Nothing - |> onChangedSidebar - ) + (Element.height <| Element.px <| window.height) + :: (case dialog of + Just dialogConfig -> + Widget.modal dialogConfig - Nothing -> - Nothing - |> onChangedSidebar - , content = Element.none - } + Nothing -> + Widget.modal + { onDismiss = + Nothing + |> onChangedSidebar + |> Just + , content = sheet + } + ) else [] - , [ Element.inFront sheet - , Element.inFront <| - case dialog of - Just element -> - element.content - - Nothing -> - Element.none - ] ] ) diff --git a/src/Widget.elm b/src/Widget.elm index 65e0abd..05227c6 100644 --- a/src/Widget.elm +++ b/src/Widget.elm @@ -1,11 +1,11 @@ module Widget exposing - ( select, multiSelect, collapsable, carousel, scrim, tab - , dialog + ( select, multiSelect, collapsable, carousel, modal, tab, dialog + , Dialog, MultiSelect, Select, selectButton ) {-| This module contains functions for displaying data. -@docs select, multiSelect, collapsable, carousel, scrim, tab +@docs select, multiSelect, collapsable, carousel, modal, tab, dialog # DEPRECATED @@ -20,67 +20,97 @@ import Element.Background as Background import Element.Events as Events import Element.Input as Input 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. -} select : - { selected : Maybe a - , options : List a - , label : a -> Element msg - , onChange : a -> msg - , attributes : Bool -> List (Attribute msg) - } - -> List (Element msg) -select { selected, options, label, onChange, attributes } = + Select msg + -> List ( Bool, Button msg ) +select { selected, options, onSelect } = options - |> List.map - (\a -> - Input.button (attributes (selected == Just a)) - { onPress = a |> onChange |> Just - , label = label a - } + |> List.indexedMap + (\i a -> + ( selected == Just i + , { onPress = i |> onSelect + , text = a.text + , icon = a.icon + } + ) ) {-| Selects multible options. This can be used for checkboxes. -} multiSelect : - { selected : Set comparable - , options : List comparable - , label : comparable -> Element msg - , onChange : comparable -> msg - , attributes : Bool -> List (Attribute msg) - } - -> List (Element msg) -multiSelect { selected, options, label, onChange, attributes } = + MultiSelect msg + -> List ( Bool, Button msg ) +multiSelect { selected, options, onSelect } = options - |> List.map - (\a -> - Input.button (attributes (selected |> Set.member a)) - { onPress = a |> onChange |> Just - , label = - label a - } + |> List.indexedMap + (\i a -> + ( selected |> Set.member i + , { onPress = i |> onSelect + , text = a.text + , icon = a.icon + } + ) ) {-| 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 @@ -107,81 +137,92 @@ collapsable { onToggle, isCollapsed, label, content } = {-| Displayes a list of contents in a tab -} tab : - List (Attribute msg) - -> - { selected : a - , options : List a - , onChange : a -> msg - , label : a -> Element msg - , content : a -> Element msg - , attributes : Bool -> List (Attribute msg) - } + { style + | tabButton : ButtonStyle msg + , tabRow : List (Attribute msg) + } + -> Select msg + -> (Maybe Int -> Element msg) -> Element msg -tab atts { selected, options, onChange, label, content, attributes } = - [ select - { selected = Just selected - , options = options - , label = label - , onChange = onChange - , attributes = attributes - } - |> Element.row atts - , content selected +tab style options content = + [ options + |> select + |> List.map (selectButton style.tabButton) + |> Element.row style.tabRow + , options.selected + |> content ] |> Element.column [] -{-| DEPRECATED. Use scrim instead. --} dialog : - { onDismiss : Maybe msg - , content : Element msg + { containerColumn : List (Attribute msg) + , title : List (Attribute msg) + , buttonRow : List (Attribute msg) + , accept : ButtonStyle msg + , dismiss : ButtonStyle 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 [] - ) + -> Dialog msg + -> { onDismiss : Maybe msg, content : Element msg } +dialog style { title, body, accept, dismiss } = + { onDismiss = + case ( accept, dismiss ) of + ( Nothing, Nothing ) -> + Nothing + + ( Nothing, Just { onPress } ) -> + onPress + + ( Just _, _ ) -> + Nothing + , content = + 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 - -{-| A scrim to block the interaction with the site. Usefull for modals and side panels - -If the scrim is clicked a message may be send. Also one can place an element infront. - - 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 + ( Just acceptButton, Just dismissButton ) -> + [ dismissButton + |> Button.viewTextOnly style.dismiss + , acceptButton + |> Button.viewTextOnly style.accept ] - } + + _ -> + [] + ) ] + } + + +{-| 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) -scrim { onDismiss, content } = - Element.el +modal : { onDismiss : Maybe msg, content : Element msg } -> List (Attribute msg) +modal { onDismiss, content } = + [ Element.el ([ Element.width <| Element.fill , Element.height <| Element.fill , Background.color <| Element.rgba255 0 0 0 0.5 @@ -193,7 +234,8 @@ scrim { onDismiss, content } = ) content |> Element.inFront - |> List.singleton + , Element.clip + ] {-| A Carousel circles through a non empty list of contents. diff --git a/src/Widget/Button.elm b/src/Widget/Button.elm new file mode 100644 index 0000000..6be1e10 --- /dev/null +++ b/src/Widget/Button.elm @@ -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 + ] + } diff --git a/src/Widget/ScrollingNav.elm b/src/Widget/ScrollingNav.elm index dab0dff..3c0f141 100644 --- a/src/Widget/ScrollingNav.elm +++ b/src/Widget/ScrollingNav.elm @@ -1,7 +1,7 @@ module Widget.ScrollingNav exposing - ( Model, init, view, viewSections, current + ( Model, init, view, current , jumpTo, syncPositions - , getPos, jumpToWithOffset, setPos + , getPos, jumpToWithOffset, setPos, toSelect ) {-| 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 Element exposing (Attribute, Element) +import Element exposing (Element) import Framework.Grid as Grid import Html.Attributes as Attributes import IntDict exposing (IntDict) import Task exposing (Task) +import Widget exposing (Select) {-| -} type alias Model section = - { labels : section -> String + { toString : section -> String + , fromString : String -> Maybe section , positions : IntDict String , arrangement : List section , scrollPos : Int @@ -39,13 +41,15 @@ type alias Model section = {-| The intial state include the labels and the arrangement of the sections -} init : - { labels : section -> String + { toString : section -> String + , fromString : String -> Maybe section , arrangement : List section , toMsg : Result Dom.Error (Model section -> Model section) -> msg } -> ( Model section, Cmd msg ) -init { labels, arrangement, toMsg } = - { labels = labels +init { toString, fromString, arrangement, toMsg } = + { toString = toString + , fromString = fromString , positions = IntDict.empty , arrangement = arrangement , scrollPos = 0 @@ -82,8 +86,8 @@ jumpTo : } -> Model section -> Cmd msg -jumpTo { section, onChange } { labels } = - Dom.getElement (section |> labels) +jumpTo { section, onChange } { toString } = + Dom.getElement (section |> toString) |> Task.andThen (\{ element } -> Dom.setViewport 0 element.y @@ -100,8 +104,8 @@ jumpToWithOffset : } -> Model section -> Cmd msg -jumpToWithOffset { offset, section, onChange } { labels } = - Dom.getElement (section |> labels) +jumpToWithOffset { offset, section, onChange } { toString } = + Dom.getElement (section |> toString) |> Task.andThen (\{ element } -> 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 { labels, arrangement } = +syncPositions { toString, arrangement } = arrangement |> List.map (\label -> - Dom.getElement (labels label) + Dom.getElement (toString label) |> Task.map (\x -> ( x.element.y |> round @@ -133,7 +137,7 @@ syncPositions { labels, arrangement } = | positions = model.positions |> IntDict.insert pos - (label |> model.labels) + (label |> model.toString) } ) m @@ -151,27 +155,29 @@ current fromString { positions, scrollPos } = |> Maybe.andThen fromString -{-| -} -viewSections : - { label : String -> Element msg - , fromString : String -> Maybe section - , onSelect : section -> msg - , attributes : Bool -> List (Attribute msg) - } - -> Model section - -> - { selected : Maybe section - , options : List section - , label : section -> Element msg - , onChange : section -> msg - , attributes : Bool -> List (Attribute msg) - } -viewSections { label, fromString, onSelect, attributes } ({ arrangement, labels } as model) = - { selected = model |> current fromString - , options = arrangement - , label = \elem -> label (elem |> labels) - , onChange = onSelect - , attributes = attributes +toSelect : (Int -> Maybe msg) -> Model section -> Select msg +toSelect onSelect ({ arrangement, toString, fromString } as model) = + { selected = + arrangement + |> List.indexedMap (\i s -> ( i, s )) + |> List.filterMap + (\( i, s ) -> + if Just s == current fromString model then + Just i + + else + Nothing + ) + |> List.head + , options = + arrangement + |> List.map + (\s -> + { text = toString s + , icon = Element.none + } + ) + , onSelect = onSelect } @@ -180,13 +186,13 @@ view : (section -> Element msg) -> Model section -> Element msg -view asElement { labels, arrangement } = +view asElement { toString, arrangement } = arrangement |> List.map (\header -> Element.el [ header - |> labels + |> toString |> Attributes.id |> Element.htmlAttribute , Element.width <| Element.fill diff --git a/src/Widget/Snackbar.elm b/src/Widget/Snackbar.elm index aa2eadc..464f3fd 100644 --- a/src/Widget/Snackbar.elm +++ b/src/Widget/Snackbar.elm @@ -1,6 +1,7 @@ module Widget.Snackbar exposing ( Model, init, current, timePassed , insert, insertFor, dismiss + , Message, view ) {-| 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 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 @@ -92,3 +102,31 @@ timePassed ms model = current : Model a -> Maybe a current model = 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 + ) + )