mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-09-21 12:19:03 +03:00
restore modules that were actually used
This commit is contained in:
parent
c576d0d543
commit
18af70fe96
17
elm.json
17
elm.json
@ -7,11 +7,19 @@
|
|||||||
"exposed-modules": [
|
"exposed-modules": [
|
||||||
"Nri.Ui",
|
"Nri.Ui",
|
||||||
"Nri.Ui.Accordion.V1",
|
"Nri.Ui.Accordion.V1",
|
||||||
|
"Nri.Ui.Alert.V2",
|
||||||
|
"Nri.Ui.Alert.V3",
|
||||||
"Nri.Ui.Alert.V4",
|
"Nri.Ui.Alert.V4",
|
||||||
"Nri.Ui.AssetPath",
|
"Nri.Ui.AssetPath",
|
||||||
"Nri.Ui.AssignmentIcon.V1",
|
"Nri.Ui.AssignmentIcon.V1",
|
||||||
|
"Nri.Ui.BannerAlert.V2",
|
||||||
|
"Nri.Ui.BannerAlert.V3",
|
||||||
"Nri.Ui.BannerAlert.V6",
|
"Nri.Ui.BannerAlert.V6",
|
||||||
|
"Nri.Ui.Button.V3",
|
||||||
|
"Nri.Ui.Button.V4",
|
||||||
"Nri.Ui.Button.V5",
|
"Nri.Ui.Button.V5",
|
||||||
|
"Nri.Ui.Button.V6",
|
||||||
|
"Nri.Ui.Button.V7",
|
||||||
"Nri.Ui.Button.V8",
|
"Nri.Ui.Button.V8",
|
||||||
"Nri.Ui.Button.V10",
|
"Nri.Ui.Button.V10",
|
||||||
"Nri.Ui.Callout.V1",
|
"Nri.Ui.Callout.V1",
|
||||||
@ -29,6 +37,7 @@
|
|||||||
"Nri.Ui.Dropdown.V2",
|
"Nri.Ui.Dropdown.V2",
|
||||||
"Nri.Ui.Effects.V1",
|
"Nri.Ui.Effects.V1",
|
||||||
"Nri.Ui.Fonts.V1",
|
"Nri.Ui.Fonts.V1",
|
||||||
|
"Nri.Ui.Heading.V1",
|
||||||
"Nri.Ui.Heading.V2",
|
"Nri.Ui.Heading.V2",
|
||||||
"Nri.Ui.Html.Attributes.V2",
|
"Nri.Ui.Html.Attributes.V2",
|
||||||
"Nri.Ui.Html.V3",
|
"Nri.Ui.Html.V3",
|
||||||
@ -38,7 +47,10 @@
|
|||||||
"Nri.Ui.InputStyles.V2",
|
"Nri.Ui.InputStyles.V2",
|
||||||
"Nri.Ui.Logo.V1",
|
"Nri.Ui.Logo.V1",
|
||||||
"Nri.Ui.MasteryIcon.V1",
|
"Nri.Ui.MasteryIcon.V1",
|
||||||
|
"Nri.Ui.Modal.V2",
|
||||||
|
"Nri.Ui.Modal.V3",
|
||||||
"Nri.Ui.Modal.V8",
|
"Nri.Ui.Modal.V8",
|
||||||
|
"Nri.Ui.Page.V2",
|
||||||
"Nri.Ui.Page.V3",
|
"Nri.Ui.Page.V3",
|
||||||
"Nri.Ui.Palette.V1",
|
"Nri.Ui.Palette.V1",
|
||||||
"Nri.Ui.Pennant.V2",
|
"Nri.Ui.Pennant.V2",
|
||||||
@ -46,6 +58,7 @@
|
|||||||
"Nri.Ui.PremiumCheckbox.V2",
|
"Nri.Ui.PremiumCheckbox.V2",
|
||||||
"Nri.Ui.PremiumCheckbox.V3",
|
"Nri.Ui.PremiumCheckbox.V3",
|
||||||
"Nri.Ui.PremiumCheckbox.V6",
|
"Nri.Ui.PremiumCheckbox.V6",
|
||||||
|
"Nri.Ui.SegmentedControl.V6",
|
||||||
"Nri.Ui.SegmentedControl.V7",
|
"Nri.Ui.SegmentedControl.V7",
|
||||||
"Nri.Ui.SegmentedControl.V8",
|
"Nri.Ui.SegmentedControl.V8",
|
||||||
"Nri.Ui.Select.V5",
|
"Nri.Ui.Select.V5",
|
||||||
@ -60,9 +73,11 @@
|
|||||||
"Nri.Ui.Table.V4",
|
"Nri.Ui.Table.V4",
|
||||||
"Nri.Ui.Table.V5",
|
"Nri.Ui.Table.V5",
|
||||||
"Nri.Ui.Tabs.V4",
|
"Nri.Ui.Tabs.V4",
|
||||||
|
"Nri.Ui.Tabs.V3",
|
||||||
"Nri.Ui.Text.V2",
|
"Nri.Ui.Text.V2",
|
||||||
"Nri.Ui.Text.V4",
|
"Nri.Ui.Text.V4",
|
||||||
"Nri.Ui.Text.Writing.V1",
|
"Nri.Ui.Text.Writing.V1",
|
||||||
|
"Nri.Ui.TextArea.V3",
|
||||||
"Nri.Ui.TextArea.V4",
|
"Nri.Ui.TextArea.V4",
|
||||||
"Nri.Ui.TextInput.V3",
|
"Nri.Ui.TextInput.V3",
|
||||||
"Nri.Ui.TextInput.V4",
|
"Nri.Ui.TextInput.V4",
|
||||||
@ -90,4 +105,4 @@
|
|||||||
"avh4/elm-program-test": "3.1.0 <= v < 4.0.0",
|
"avh4/elm-program-test": "3.1.0 <= v < 4.0.0",
|
||||||
"elm-explorations/test": "1.2.0 <= v < 2.0.0"
|
"elm-explorations/test": "1.2.0 <= v < 2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
filename,name,version
|
filename,name,version
|
||||||
src/EventExtras.elm,Html,DEPRECATED
|
src/EventExtras.elm,Html,DEPRECATED
|
||||||
|
src/Nri/Ui/Tabs/V3.elm,Accessibility.Aria,DEPRECATED
|
||||||
|
src/Nri/Ui/Tabs/V3.elm,Accessibility.Key,DEPRECATED
|
||||||
|
src/Nri/Ui/Tabs/V3.elm,Accessibility.Role,DEPRECATED
|
||||||
|
src/Nri/Ui/Tabs/V3.elm,Accessibility.Widget,DEPRECATED
|
||||||
src/Nri/Ui/Page/V3.elm,Nri.Ui.Button,5
|
src/Nri/Ui/Page/V3.elm,Nri.Ui.Button,5
|
||||||
src/Nri/Ui/Page/V3.elm,Nri.Ui.Text,2
|
src/Nri/Ui/Page/V3.elm,Nri.Ui.Text,2
|
||||||
|
src/Nri/Ui/Page/V2.elm,Nri.Ui.Button,5
|
||||||
|
src/Nri/Ui/Page/V2.elm,Nri.Ui.Text,2
|
||||||
src/Nri/Ui/PremiumCheckbox/V3.elm,Nri.Ui.Checkbox,4
|
src/Nri/Ui/PremiumCheckbox/V3.elm,Nri.Ui.Checkbox,4
|
||||||
src/Nri/Ui/PremiumCheckbox/V2.elm,Nri.Ui.Checkbox,3
|
src/Nri/Ui/PremiumCheckbox/V2.elm,Nri.Ui.Checkbox,3
|
||||||
src/Nri/Ui/PremiumCheckbox/V1.elm,Nri.Ui.Checkbox,3
|
src/Nri/Ui/PremiumCheckbox/V1.elm,Nri.Ui.Checkbox,3
|
||||||
@ -11,14 +17,22 @@ src/Nri/Ui/SlideModal/V2.elm,Nri.Ui.Text,2
|
|||||||
src/Nri/Ui/SlideModal/V1.elm,Nri.Ui.Button,8
|
src/Nri/Ui/SlideModal/V1.elm,Nri.Ui.Button,8
|
||||||
src/Nri/Ui/SlideModal/V1.elm,Nri.Ui.Icon,3
|
src/Nri/Ui/SlideModal/V1.elm,Nri.Ui.Icon,3
|
||||||
src/Nri/Ui/SlideModal/V1.elm,Nri.Ui.Text,2
|
src/Nri/Ui/SlideModal/V1.elm,Nri.Ui.Text,2
|
||||||
|
src/Nri/Ui/Alert/V3.elm,Nri.Ui.Icon,3
|
||||||
|
src/Nri/Ui/Alert/V2.elm,Nri.Ui.Icon,3
|
||||||
src/Nri/Ui/Alert/V4.elm,Nri.Ui.Icon,3
|
src/Nri/Ui/Alert/V4.elm,Nri.Ui.Icon,3
|
||||||
src/Nri/Ui/SortableTable/V1.elm,Nri.Ui.Table,4
|
src/Nri/Ui/SortableTable/V1.elm,Nri.Ui.Table,4
|
||||||
src/Nri/Ui/Html/Attributes/Extra.elm,Html,DEPRECATED
|
src/Nri/Ui/Html/Attributes/Extra.elm,Html,DEPRECATED
|
||||||
src/Nri/Ui/Button/V8.elm,Html,DEPRECATED
|
src/Nri/Ui/Button/V8.elm,Html,DEPRECATED
|
||||||
|
src/Nri/Ui/Button/V3.elm,Nri.Ui.Icon,3
|
||||||
src/Nri/Ui/Button/V5.elm,Nri.Ui.Icon,3
|
src/Nri/Ui/Button/V5.elm,Nri.Ui.Icon,3
|
||||||
|
src/Nri/Ui/Button/V4.elm,Nri.Ui.Icon,3
|
||||||
|
src/Nri/Ui/Button/V6.elm,Nri.Ui.Icon,4
|
||||||
|
src/Nri/Ui/Button/V7.elm,Nri.Ui.Icon,4
|
||||||
|
src/Nri/Ui/SegmentedControl/V6.elm,Nri.Ui.Icon,3
|
||||||
src/Nri/Ui/Icon/V3.elm,Accessibility.Role,DEPRECATED
|
src/Nri/Ui/Icon/V3.elm,Accessibility.Role,DEPRECATED
|
||||||
src/Nri/Ui/Icon/V3.elm,Html,DEPRECATED
|
src/Nri/Ui/Icon/V3.elm,Html,DEPRECATED
|
||||||
src/Nri/Ui/Icon/V5.elm,Accessibility.Role,DEPRECATED
|
src/Nri/Ui/Icon/V5.elm,Accessibility.Role,DEPRECATED
|
||||||
src/Nri/Ui/Icon/V5.elm,Html,DEPRECATED
|
src/Nri/Ui/Icon/V5.elm,Html,DEPRECATED
|
||||||
src/Nri/Ui/Icon/V4.elm,Accessibility.Role,DEPRECATED
|
src/Nri/Ui/Icon/V4.elm,Accessibility.Role,DEPRECATED
|
||||||
src/Nri/Ui/Icon/V4.elm,Html,DEPRECATED
|
src/Nri/Ui/Icon/V4.elm,Html,DEPRECATED
|
||||||
|
src/Nri/Ui/Modal/V3.elm,Nri.Ui.Icon,3
|
||||||
|
|
121
src/Nri/Ui/Alert/V2.elm
Normal file
121
src/Nri/Ui/Alert/V2.elm
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
module Nri.Ui.Alert.V2 exposing
|
||||||
|
( error
|
||||||
|
, success
|
||||||
|
, tip
|
||||||
|
, warning
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| UI components that highlight information to the user.
|
||||||
|
|
||||||
|
@docs error
|
||||||
|
@docs success
|
||||||
|
@docs tip
|
||||||
|
@docs warning
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (Html)
|
||||||
|
import Css
|
||||||
|
import Css.Global
|
||||||
|
import Html.Styled exposing (fromUnstyled)
|
||||||
|
import Html.Styled.Attributes exposing (css)
|
||||||
|
import Markdown
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1 as Fonts
|
||||||
|
import Nri.Ui.Icon.V3 as Icon
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
error : { r | exclamation : String } -> String -> Html msg
|
||||||
|
error assets content =
|
||||||
|
alert
|
||||||
|
[ iconContainer [ Css.color Colors.purple ]
|
||||||
|
(Icon.decorativeIcon (Icon.exclamation assets))
|
||||||
|
, viewAlertContent Colors.purpleDark content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
success : { r | checkmark : String } -> String -> Html msg
|
||||||
|
success assets content =
|
||||||
|
alert
|
||||||
|
[ iconContainer
|
||||||
|
[ Css.color Colors.white
|
||||||
|
, Css.backgroundColor Colors.green
|
||||||
|
, Css.Global.children [ Css.Global.svg [ Css.maxWidth (Css.px 12) ] ]
|
||||||
|
]
|
||||||
|
(Icon.decorativeIcon (Icon.checkMarkSvg assets))
|
||||||
|
, viewAlertContent Colors.greenDarkest content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
tip : { r | bulb : String } -> String -> Html msg
|
||||||
|
tip assets content =
|
||||||
|
alert
|
||||||
|
[ iconContainer [ Css.color Colors.yellow ]
|
||||||
|
(Icon.decorativeIcon (Icon.bulb assets))
|
||||||
|
, viewAlertContent Colors.navy content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
warning : { r | exclamation : String } -> String -> Html msg
|
||||||
|
warning assets content =
|
||||||
|
alert
|
||||||
|
[ iconContainer [ Css.color Colors.red ]
|
||||||
|
(Icon.decorativeIcon (Icon.exclamation assets))
|
||||||
|
, viewAlertContent Colors.red content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
alert : List (Html msg) -> Html msg
|
||||||
|
alert =
|
||||||
|
Nri.Ui.styled Html.div
|
||||||
|
"Nri-Ui-Alert-V2__alert"
|
||||||
|
[ Css.displayFlex
|
||||||
|
, Css.justifyContent Css.start
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.paddingTop (Css.px 6)
|
||||||
|
, Css.paddingBottom (Css.px 8)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
iconContainer : List Css.Style -> Html msg -> Html msg
|
||||||
|
iconContainer styles icon =
|
||||||
|
Nri.Ui.styled Html.div
|
||||||
|
"Nri-Ui-Alert-V2__iconContainer"
|
||||||
|
(styles
|
||||||
|
++ [ -- Content positioning
|
||||||
|
Css.displayFlex
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.marginRight (Css.px 5)
|
||||||
|
|
||||||
|
-- Size
|
||||||
|
, Css.borderRadius (Css.px 13)
|
||||||
|
, Css.maxHeight (Css.px 20)
|
||||||
|
, Css.maxWidth (Css.px 20)
|
||||||
|
, Css.minHeight (Css.px 20)
|
||||||
|
, Css.minWidth (Css.px 20)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
[ icon ]
|
||||||
|
|
||||||
|
|
||||||
|
viewAlertContent : Css.ColorValue compatible -> String -> Html.Styled.Html msg
|
||||||
|
viewAlertContent color content =
|
||||||
|
Nri.Ui.styled Html.div
|
||||||
|
"Nri-Ui-Alert-V2__viewAlertContent"
|
||||||
|
[ Css.color color
|
||||||
|
, Fonts.baseFont
|
||||||
|
, Css.fontSize (Css.px 13)
|
||||||
|
, Css.lineHeight (Css.num 1.2)
|
||||||
|
, Css.listStyleType Css.none
|
||||||
|
, Css.Global.descendants [ Css.Global.p [ Css.margin Css.zero ] ]
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
(Markdown.toHtml Nothing content |> List.map fromUnstyled)
|
156
src/Nri/Ui/Alert/V3.elm
Normal file
156
src/Nri/Ui/Alert/V3.elm
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
module Nri.Ui.Alert.V3 exposing
|
||||||
|
( error
|
||||||
|
, success
|
||||||
|
, tip
|
||||||
|
, warning
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| UI components that highlight information to the user.
|
||||||
|
|
||||||
|
@docs error
|
||||||
|
@docs success
|
||||||
|
@docs tip
|
||||||
|
@docs warning
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (Html)
|
||||||
|
import Css
|
||||||
|
import Css.Global
|
||||||
|
import Html.Styled exposing (fromUnstyled)
|
||||||
|
import Html.Styled.Attributes exposing (css)
|
||||||
|
import Markdown
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1 as Fonts
|
||||||
|
import Nri.Ui.Icon.V3 as Icon
|
||||||
|
import Nri.Ui.SpriteSheet exposing (bulb, checkmark, exclamationMark)
|
||||||
|
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
error : String -> Html msg
|
||||||
|
error content =
|
||||||
|
alert
|
||||||
|
[ exclamation Colors.purple
|
||||||
|
, viewAlertContent Colors.purpleDark content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
success : String -> Html msg
|
||||||
|
success content =
|
||||||
|
alert
|
||||||
|
[ iconContainer
|
||||||
|
[ Css.color Colors.white
|
||||||
|
, Css.backgroundColor Colors.green
|
||||||
|
]
|
||||||
|
(Html.div
|
||||||
|
[ css
|
||||||
|
[ Css.width (Css.px 12)
|
||||||
|
, Css.height (Css.px 12)
|
||||||
|
, Css.margin Css.auto
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ NriSvg.toHtml checkmark ]
|
||||||
|
)
|
||||||
|
, viewAlertContent Colors.greenDarkest content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
tip : String -> Html msg
|
||||||
|
tip content =
|
||||||
|
alert
|
||||||
|
[ iconContainer [ Css.color Colors.yellow ] (NriSvg.toHtml bulb)
|
||||||
|
, viewAlertContent Colors.navy content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
warning : String -> Html msg
|
||||||
|
warning content =
|
||||||
|
alert
|
||||||
|
[ exclamation Colors.red
|
||||||
|
, viewAlertContent Colors.red content
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
alert : List (Html msg) -> Html msg
|
||||||
|
alert =
|
||||||
|
Nri.Ui.styled Html.div
|
||||||
|
"Nri-Ui-Alert-V3__alert"
|
||||||
|
[ Css.displayFlex
|
||||||
|
, Css.justifyContent Css.start
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.paddingTop (Css.px 6)
|
||||||
|
, Css.paddingBottom (Css.px 8)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
exclamation : Css.Color -> Html msg
|
||||||
|
exclamation backgroundColor =
|
||||||
|
iconContainer
|
||||||
|
[ Css.color Colors.white
|
||||||
|
, Css.backgroundColor backgroundColor
|
||||||
|
]
|
||||||
|
(Html.div
|
||||||
|
[ css [ Css.marginTop (Css.px 1), Css.height (Css.px 13) ] ]
|
||||||
|
[ NriSvg.toHtml exclamationMark ]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
iconContainer : List Css.Style -> Html msg -> Html msg
|
||||||
|
iconContainer styles icon =
|
||||||
|
Nri.Ui.styled Html.div
|
||||||
|
"Nri-Ui-Alert-V3__iconContainer"
|
||||||
|
(styles
|
||||||
|
++ [ -- Content positioning
|
||||||
|
Css.marginRight (Css.px 5)
|
||||||
|
|
||||||
|
-- Size
|
||||||
|
, Css.borderRadius (Css.px 13)
|
||||||
|
, Css.lineHeight iconContainerSize
|
||||||
|
, Css.maxHeight iconContainerSize
|
||||||
|
, Css.maxWidth iconContainerSize
|
||||||
|
, Css.minHeight iconContainerSize
|
||||||
|
, Css.minWidth iconContainerSize
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
[ icon ]
|
||||||
|
|
||||||
|
|
||||||
|
viewAlertContent : Css.ColorValue compatible -> String -> Html.Styled.Html msg
|
||||||
|
viewAlertContent color content =
|
||||||
|
Nri.Ui.styled Html.div
|
||||||
|
"Nri-Ui-Alert-V3__viewAlertContent"
|
||||||
|
[ Css.color color
|
||||||
|
, Fonts.baseFont
|
||||||
|
, Css.fontSize (Css.px 13)
|
||||||
|
, Css.lineHeight iconContainerSize
|
||||||
|
, Css.listStyleType Css.none
|
||||||
|
|
||||||
|
-- This global selector and overrides are necessary due to
|
||||||
|
-- old stylesheets used on the monolith that set the
|
||||||
|
-- `.txt p { font-size: 18px; }` -- without these overrides,
|
||||||
|
-- we may see giant ugly alerts.
|
||||||
|
-- Remove these if you want to! but be emotionally prepped
|
||||||
|
-- to deal with visual regressions. 🙏
|
||||||
|
, Css.Global.descendants
|
||||||
|
[ Css.Global.p
|
||||||
|
[ Css.margin Css.zero
|
||||||
|
, Css.lineHeight iconContainerSize
|
||||||
|
, Css.fontSize (Css.px 13)
|
||||||
|
, Fonts.baseFont
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
(Markdown.toHtml Nothing content |> List.map fromUnstyled)
|
||||||
|
|
||||||
|
|
||||||
|
iconContainerSize : Css.Px
|
||||||
|
iconContainerSize =
|
||||||
|
Css.px 20
|
114
src/Nri/Ui/BannerAlert/V2.elm
Normal file
114
src/Nri/Ui/BannerAlert/V2.elm
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
module Nri.Ui.BannerAlert.V2 exposing
|
||||||
|
( error
|
||||||
|
, neutral
|
||||||
|
, success
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
@docs error
|
||||||
|
@docs neutral
|
||||||
|
@docs success
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Accessibility
|
||||||
|
import Css exposing (..)
|
||||||
|
import Css.Global exposing (Snippet, children, descendants, everything, selector)
|
||||||
|
import Html.Styled as Html exposing (Html)
|
||||||
|
import Nri.Ui.Colors.V1
|
||||||
|
import Nri.Ui.Fonts.V1
|
||||||
|
|
||||||
|
|
||||||
|
{-| A banner to show error alerts
|
||||||
|
-}
|
||||||
|
error : String -> Html msg
|
||||||
|
error =
|
||||||
|
banner errorStyles
|
||||||
|
|
||||||
|
|
||||||
|
{-| A banner to show neutral alerts
|
||||||
|
-}
|
||||||
|
neutral : String -> Html msg
|
||||||
|
neutral =
|
||||||
|
banner neutralStyles
|
||||||
|
|
||||||
|
|
||||||
|
{-| A banner for success alerts
|
||||||
|
-}
|
||||||
|
success : String -> Html msg
|
||||||
|
success =
|
||||||
|
banner successStyles
|
||||||
|
|
||||||
|
|
||||||
|
banner : Css.Style -> String -> Html msg
|
||||||
|
banner bannerType alertMessage =
|
||||||
|
Html.styled Accessibility.div
|
||||||
|
[ bannerStyles, bannerType ]
|
||||||
|
[]
|
||||||
|
[ notification alertMessage ]
|
||||||
|
|
||||||
|
|
||||||
|
notification : String -> Html msg
|
||||||
|
notification message =
|
||||||
|
Html.styled Html.div [ alertMessageStyles ] [] [ Accessibility.text message ]
|
||||||
|
|
||||||
|
|
||||||
|
type CssClasses
|
||||||
|
= AlertMessage
|
||||||
|
| Banner
|
||||||
|
| Error
|
||||||
|
| Neutral
|
||||||
|
| Success
|
||||||
|
|
||||||
|
|
||||||
|
alertMessageStyles : Style
|
||||||
|
alertMessageStyles =
|
||||||
|
batch
|
||||||
|
[ Css.fontSize (Css.px 20)
|
||||||
|
, Css.fontWeight (Css.int 700)
|
||||||
|
, Css.lineHeight (Css.px 25)
|
||||||
|
, Css.maxWidth (Css.px 600)
|
||||||
|
, Nri.Ui.Fonts.V1.baseFont
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
bannerStyles : Style
|
||||||
|
bannerStyles =
|
||||||
|
batch
|
||||||
|
[ Css.alignItems Css.center
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
, Css.padding (Css.px 20)
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
, Css.Global.children
|
||||||
|
[ Css.Global.button
|
||||||
|
[ Css.position Css.absolute
|
||||||
|
, Css.right (Css.px 15)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
errorStyles : Style
|
||||||
|
errorStyles =
|
||||||
|
batch
|
||||||
|
[ Css.backgroundColor Nri.Ui.Colors.V1.purpleLight
|
||||||
|
, Css.color Nri.Ui.Colors.V1.purpleDark
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
neutralStyles : Style
|
||||||
|
neutralStyles =
|
||||||
|
batch
|
||||||
|
[ Css.backgroundColor Nri.Ui.Colors.V1.frost
|
||||||
|
, Css.color Nri.Ui.Colors.V1.navy
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
successStyles : Style
|
||||||
|
successStyles =
|
||||||
|
batch
|
||||||
|
[ Css.backgroundColor Nri.Ui.Colors.V1.greenLightest
|
||||||
|
, Css.color Nri.Ui.Colors.V1.greenDarkest
|
||||||
|
]
|
138
src/Nri/Ui/BannerAlert/V3.elm
Normal file
138
src/Nri/Ui/BannerAlert/V3.elm
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
module Nri.Ui.BannerAlert.V3 exposing (error, neutral, success)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
@docs error, neutral, success
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (Html)
|
||||||
|
import Css
|
||||||
|
import Css.Global
|
||||||
|
import Html.Styled.Attributes as Attributes exposing (css)
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1
|
||||||
|
import Nri.Ui.SpriteSheet exposing (bulb, checkmark, exclamationMark)
|
||||||
|
import Nri.Ui.Svg.V1 as NriSvg exposing (Svg)
|
||||||
|
|
||||||
|
|
||||||
|
{-| A banner to show error alerts
|
||||||
|
-}
|
||||||
|
error : String -> Html msg
|
||||||
|
error =
|
||||||
|
banner
|
||||||
|
{ backgroundColor = Colors.purpleLight
|
||||||
|
, color = Colors.purpleDark
|
||||||
|
, icon =
|
||||||
|
{ backgroundColor = Colors.purple
|
||||||
|
, height = Css.px 25
|
||||||
|
, asset = exclamationMark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A banner to show neutral alerts
|
||||||
|
-}
|
||||||
|
neutral : String -> Html msg
|
||||||
|
neutral =
|
||||||
|
banner
|
||||||
|
{ backgroundColor = Colors.frost
|
||||||
|
, color = Colors.navy
|
||||||
|
, icon =
|
||||||
|
{ backgroundColor = Colors.navy
|
||||||
|
, height = Css.px 32
|
||||||
|
, asset = bulb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A banner for success alerts
|
||||||
|
-}
|
||||||
|
success : String -> Html msg
|
||||||
|
success =
|
||||||
|
banner
|
||||||
|
{ backgroundColor = Colors.greenLightest
|
||||||
|
, color = Colors.greenDarkest
|
||||||
|
, icon =
|
||||||
|
{ backgroundColor = Colors.green
|
||||||
|
, height = Css.px 20
|
||||||
|
, asset = checkmark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Config =
|
||||||
|
{ color : Css.Color
|
||||||
|
, backgroundColor : Css.Color
|
||||||
|
, icon : IconConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
banner : Config -> String -> Html msg
|
||||||
|
banner config alertMessage =
|
||||||
|
Html.div
|
||||||
|
[ css
|
||||||
|
[ Css.alignItems Css.center
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
, Css.padding (Css.px 20)
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
, Css.Global.children
|
||||||
|
[ Css.Global.button
|
||||||
|
[ Css.position Css.absolute
|
||||||
|
, Css.right (Css.px 15)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, Css.backgroundColor config.backgroundColor
|
||||||
|
, Css.color config.color
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ icon config.icon
|
||||||
|
, notification alertMessage
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
type alias IconConfig =
|
||||||
|
{ backgroundColor : Css.Color
|
||||||
|
, height : Css.Px
|
||||||
|
, asset : Svg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
icon : IconConfig -> Html msg
|
||||||
|
icon config =
|
||||||
|
Html.div
|
||||||
|
[ css
|
||||||
|
[ Css.boxSizing Css.borderBox
|
||||||
|
, Css.borderRadius (Css.pct 50)
|
||||||
|
, Css.color Colors.white
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
, Css.width (Css.px 50)
|
||||||
|
, Css.height (Css.px 50)
|
||||||
|
, Css.marginRight (Css.px 20)
|
||||||
|
, Css.padding (Css.px 8)
|
||||||
|
, Css.flexShrink (Css.num 0)
|
||||||
|
, Css.backgroundColor config.backgroundColor
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ Html.div
|
||||||
|
[ css [ Css.height config.height ]
|
||||||
|
]
|
||||||
|
[ NriSvg.toHtml config.asset ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
notification : String -> Html msg
|
||||||
|
notification message =
|
||||||
|
Html.div
|
||||||
|
[ css
|
||||||
|
[ Css.fontSize (Css.px 20)
|
||||||
|
, Css.fontWeight (Css.int 700)
|
||||||
|
, Css.lineHeight (Css.px 25)
|
||||||
|
, Css.maxWidth (Css.px 600)
|
||||||
|
, Nri.Ui.Fonts.V1.baseFont
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[ Html.text message ]
|
780
src/Nri/Ui/Button/V3.elm
Normal file
780
src/Nri/Ui/Button/V3.elm
Normal file
@ -0,0 +1,780 @@
|
|||||||
|
module Nri.Ui.Button.V3 exposing
|
||||||
|
( ButtonSize(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||||
|
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
# Changes from V2:
|
||||||
|
|
||||||
|
- Uses Html.Styled
|
||||||
|
- Removes buttonDeprecated
|
||||||
|
- Removes Tiny size
|
||||||
|
- Removes one-off Active hack
|
||||||
|
- Removes "submit" button - we just used that for forms that were partially in Elm
|
||||||
|
|
||||||
|
|
||||||
|
# About:
|
||||||
|
|
||||||
|
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||||
|
action on the current page should be HTML `<button>` elements and are created here
|
||||||
|
with `*Button` functions. Buttons that take the user to a new page should be
|
||||||
|
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||||
|
should be able to use the same CSS class in all cases.
|
||||||
|
|
||||||
|
There will generally be a `*Button` and `*Link` version of each button style.
|
||||||
|
(These will be created as they are needed.)
|
||||||
|
|
||||||
|
|
||||||
|
## Common configs
|
||||||
|
|
||||||
|
@docs ButtonSize, ButtonStyle, ButtonState, ButtonContent
|
||||||
|
|
||||||
|
|
||||||
|
## `<button>` Buttons
|
||||||
|
|
||||||
|
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
|
||||||
|
|
||||||
|
## `<a>` Buttons
|
||||||
|
|
||||||
|
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||||
|
import Accessibility.Styled.Role as Role
|
||||||
|
import Accessibility.Styled.Widget as Widget
|
||||||
|
import Css exposing (Style)
|
||||||
|
import Css.Global
|
||||||
|
import EventExtras.Styled as EventExtras
|
||||||
|
import Html.Styled as Styled
|
||||||
|
import Html.Styled.Attributes as Attributes
|
||||||
|
import Html.Styled.Events as Events
|
||||||
|
import Json.Decode
|
||||||
|
import Markdown.Block
|
||||||
|
import Markdown.Inline
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||||
|
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1
|
||||||
|
import Nri.Ui.Icon.V3 as Icon exposing (IconType)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Sizes for buttons and links that have button classes
|
||||||
|
-}
|
||||||
|
type ButtonSize
|
||||||
|
= Small
|
||||||
|
| Medium
|
||||||
|
| Large
|
||||||
|
|
||||||
|
|
||||||
|
{-| Styleguide-approved styles for your buttons!
|
||||||
|
|
||||||
|
Note on borderless buttons:
|
||||||
|
A borderless button that performs an action on the current page
|
||||||
|
This button is intended to look like a link.
|
||||||
|
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonStyle
|
||||||
|
= Primary
|
||||||
|
| Secondary
|
||||||
|
| Borderless
|
||||||
|
| Danger
|
||||||
|
| Premium
|
||||||
|
|
||||||
|
|
||||||
|
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||||
|
|
||||||
|
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||||
|
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||||
|
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||||
|
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||||
|
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||||
|
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonState
|
||||||
|
= Enabled
|
||||||
|
| Unfulfilled
|
||||||
|
| Disabled
|
||||||
|
| Error
|
||||||
|
| Loading
|
||||||
|
| Success
|
||||||
|
|
||||||
|
|
||||||
|
{-| The part of a button that remains constant through different button states
|
||||||
|
-}
|
||||||
|
type alias ButtonConfig msg =
|
||||||
|
{ onClick : msg
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : Maybe Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||||
|
state may have a different label than a button in the "Error" state
|
||||||
|
-}
|
||||||
|
type alias ButtonContent =
|
||||||
|
{ label : String
|
||||||
|
, state : ButtonState
|
||||||
|
, icon : Maybe IconType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delightful button which can trigger an effect when clicked!
|
||||||
|
|
||||||
|
This button will trigger the passed-in message if the button state is:
|
||||||
|
|
||||||
|
- Enabled
|
||||||
|
- Unfulfilled
|
||||||
|
|
||||||
|
This button will be Disabled if the button state is:
|
||||||
|
|
||||||
|
- Disabled
|
||||||
|
- Error
|
||||||
|
- Loading
|
||||||
|
- Success
|
||||||
|
|
||||||
|
-}
|
||||||
|
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
button config content =
|
||||||
|
customButton [] config content
|
||||||
|
|
||||||
|
|
||||||
|
{-| Exactly the same as button but you can pass in a list of attributes
|
||||||
|
-}
|
||||||
|
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
customButton attributes config content =
|
||||||
|
let
|
||||||
|
buttonStyle_ =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
styleToColorPalette config.style
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
ErrorColors
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
LoadingColors
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
SuccessColors
|
||||||
|
|
||||||
|
disabled =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
True
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "customButton")
|
||||||
|
(buttonStyles config.size config.width buttonStyle_ Button)
|
||||||
|
([ Events.onClick config.onClick
|
||||||
|
, Attributes.disabled disabled
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
++ attributes
|
||||||
|
)
|
||||||
|
(viewLabel content.icon content.label)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- COPY TO CLIPBOARD BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Config for copyToClipboard
|
||||||
|
-}
|
||||||
|
type alias CopyToClipboardConfig =
|
||||||
|
{ size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, copyText : String
|
||||||
|
, buttonLabel : String
|
||||||
|
, withIcon : Bool
|
||||||
|
, width : Maybe Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||||
|
You will need to hook this up to clipboard.js
|
||||||
|
-}
|
||||||
|
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||||
|
copyToClipboard assets config =
|
||||||
|
let
|
||||||
|
maybeIcon =
|
||||||
|
if config.withIcon then
|
||||||
|
Just (Icon.copy assets)
|
||||||
|
|
||||||
|
else
|
||||||
|
Nothing
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "copyToClipboard")
|
||||||
|
(buttonStyles config.size config.width (styleToColorPalette config.style) Button)
|
||||||
|
[ Widget.label "Copy URL to clipboard"
|
||||||
|
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||||
|
]
|
||||||
|
(viewLabel maybeIcon config.buttonLabel)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- DELETE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
type alias DeleteButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onClick : msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delete button (blue X)
|
||||||
|
-}
|
||||||
|
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||||
|
delete assets config =
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "delete")
|
||||||
|
[ Css.display Css.inlineBlock
|
||||||
|
, Css.backgroundRepeat Css.noRepeat
|
||||||
|
, Css.backgroundColor Css.transparent
|
||||||
|
, Css.backgroundPosition Css.center
|
||||||
|
, Css.backgroundSize Css.contain
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.width (Css.px 15)
|
||||||
|
, Css.height (Css.px 15)
|
||||||
|
, Css.padding Css.zero
|
||||||
|
, Css.margin2 Css.zero (Css.px 6)
|
||||||
|
, Css.cursor Css.pointer
|
||||||
|
, Css.color Colors.azure
|
||||||
|
]
|
||||||
|
[ Events.onClick config.onClick
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
Widget.label config.label
|
||||||
|
]
|
||||||
|
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- TOGGLE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Buttons can be toggled into a pressed state and back again.
|
||||||
|
-}
|
||||||
|
type alias ToggleButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onSelect : msg
|
||||||
|
, onDeselect : msg
|
||||||
|
, pressed : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||||
|
toggleButton config =
|
||||||
|
let
|
||||||
|
toggledStyles =
|
||||||
|
if config.pressed then
|
||||||
|
[ Css.color Colors.gray20
|
||||||
|
, Css.backgroundColor Colors.glacier
|
||||||
|
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||||
|
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||||
|
, Css.fontWeight Css.bold
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "toggleButton")
|
||||||
|
(buttonStyles Medium Nothing SecondaryColors Button
|
||||||
|
++ toggledStyles
|
||||||
|
)
|
||||||
|
[ Events.onClick
|
||||||
|
(if config.pressed then
|
||||||
|
config.onDeselect
|
||||||
|
|
||||||
|
else
|
||||||
|
config.onSelect
|
||||||
|
)
|
||||||
|
, Widget.pressed <| Just config.pressed
|
||||||
|
|
||||||
|
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
, Role.button
|
||||||
|
|
||||||
|
-- Note: setting type: 'button' removes the default behavior of submit
|
||||||
|
-- equivalent to preventDefaultBehavior = false
|
||||||
|
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
(viewLabel Nothing config.label)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Inputs can be a clickable thing used in a form
|
||||||
|
-}
|
||||||
|
type alias InputConfig =
|
||||||
|
{ content : Html Never
|
||||||
|
, name : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, value : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- LINKS THAT LOOK LIKE BUTTONS
|
||||||
|
|
||||||
|
|
||||||
|
{-| Links are clickable things with a url.
|
||||||
|
|
||||||
|
NOTE: Links do not support two-line labels.
|
||||||
|
|
||||||
|
-}
|
||||||
|
type alias LinkConfig =
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, url : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : Maybe Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url
|
||||||
|
-}
|
||||||
|
link : LinkConfig -> Html msg
|
||||||
|
link =
|
||||||
|
linkBase "link" [ Attributes.target "_self" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Use this link for routing within a single page app.
|
||||||
|
|
||||||
|
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||||
|
|
||||||
|
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkSpa :
|
||||||
|
(route -> String)
|
||||||
|
-> (route -> msg)
|
||||||
|
->
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : Maybe Int
|
||||||
|
, route : route
|
||||||
|
}
|
||||||
|
-> Html msg
|
||||||
|
linkSpa toUrl toMsg config =
|
||||||
|
linkBase
|
||||||
|
"linkSpa"
|
||||||
|
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||||
|
]
|
||||||
|
{ label = config.label
|
||||||
|
, icon = config.icon
|
||||||
|
, size = config.size
|
||||||
|
, style = config.style
|
||||||
|
, width = config.width
|
||||||
|
, url = toUrl config.route
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url and have it open to an external site
|
||||||
|
-}
|
||||||
|
linkExternal : LinkConfig -> Html msg
|
||||||
|
linkExternal =
|
||||||
|
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||||
|
-}
|
||||||
|
linkWithMethod : String -> LinkConfig -> Html msg
|
||||||
|
linkWithMethod method =
|
||||||
|
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||||
|
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||||
|
-}
|
||||||
|
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkWithTracking"
|
||||||
|
[ Events.preventDefaultOn "click"
|
||||||
|
(Json.Decode.succeed ( onTrack, True ))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||||
|
|
||||||
|
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkExternalWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkExternalWithTracking"
|
||||||
|
[ Attributes.target "_blank"
|
||||||
|
, EventExtras.onClickForLinkWithHref onTrack
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Helper function for building links with an arbitrary number of Attributes
|
||||||
|
-}
|
||||||
|
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||||
|
linkBase linkFunctionName extraAttrs config =
|
||||||
|
Nri.Ui.styled Styled.a
|
||||||
|
(styledName linkFunctionName)
|
||||||
|
(Css.whiteSpace Css.noWrap
|
||||||
|
:: buttonStyles config.size config.width (styleToColorPalette config.style) Anchor
|
||||||
|
)
|
||||||
|
(Attributes.href config.url
|
||||||
|
:: extraAttrs
|
||||||
|
)
|
||||||
|
(viewLabel config.icon config.label)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HELPERS
|
||||||
|
|
||||||
|
|
||||||
|
type ColorPalette
|
||||||
|
= PrimaryColors
|
||||||
|
| SecondaryColors
|
||||||
|
| BorderlessColors
|
||||||
|
| DangerColors
|
||||||
|
| PremiumColors
|
||||||
|
| InactiveColors
|
||||||
|
| LoadingColors
|
||||||
|
| SuccessColors
|
||||||
|
| ErrorColors
|
||||||
|
|
||||||
|
|
||||||
|
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||||
|
styleToColorPalette style =
|
||||||
|
case style of
|
||||||
|
Primary ->
|
||||||
|
PrimaryColors
|
||||||
|
|
||||||
|
Secondary ->
|
||||||
|
SecondaryColors
|
||||||
|
|
||||||
|
Borderless ->
|
||||||
|
BorderlessColors
|
||||||
|
|
||||||
|
Danger ->
|
||||||
|
DangerColors
|
||||||
|
|
||||||
|
Premium ->
|
||||||
|
PremiumColors
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyles : ButtonSize -> Maybe Int -> ColorPalette -> ElementType -> List Style
|
||||||
|
buttonStyles size width colorPalette elementType =
|
||||||
|
List.concat
|
||||||
|
[ buttonStyle
|
||||||
|
, colorStyle colorPalette
|
||||||
|
, sizeStyle size width elementType
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewLabel : Maybe IconType -> String -> List (Html msg)
|
||||||
|
viewLabel icn label =
|
||||||
|
case icn of
|
||||||
|
Nothing ->
|
||||||
|
renderMarkdown label
|
||||||
|
|
||||||
|
Just iconType ->
|
||||||
|
[ Html.span [] (Icon.decorativeIcon iconType :: renderMarkdown label) ]
|
||||||
|
|
||||||
|
|
||||||
|
renderMarkdown : String -> List (Html msg)
|
||||||
|
renderMarkdown markdown =
|
||||||
|
case Markdown.Block.parse Nothing markdown of
|
||||||
|
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||||
|
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||||
|
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[ Html.text markdown ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- STYLES
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyle : List Style
|
||||||
|
buttonStyle =
|
||||||
|
[ Css.cursor Css.pointer
|
||||||
|
, Css.display Css.inlineBlock
|
||||||
|
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||||
|
Nri.Ui.Fonts.V1.baseFont
|
||||||
|
, Css.textOverflow Css.ellipsis
|
||||||
|
, Css.overflow Css.hidden
|
||||||
|
, Css.textDecoration Css.none
|
||||||
|
, Css.backgroundImage Css.none
|
||||||
|
, Css.textShadow Css.none
|
||||||
|
, Css.property "transition" "all 0.2s"
|
||||||
|
, Css.boxShadow Css.none
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.marginBottom Css.zero
|
||||||
|
, Css.hover [ Css.textDecoration Css.none ]
|
||||||
|
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
colorStyle : ColorPalette -> List Style
|
||||||
|
colorStyle colorPalette =
|
||||||
|
let
|
||||||
|
( config, additionalStyles ) =
|
||||||
|
case colorPalette of
|
||||||
|
PrimaryColors ->
|
||||||
|
( { background = Colors.azure
|
||||||
|
, hover = Colors.azureDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.azureDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SecondaryColors ->
|
||||||
|
( { background = Colors.white
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Just <| Colors.azure
|
||||||
|
, shadow = Colors.azure
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
BorderlessColors ->
|
||||||
|
( { background = Css.rgba 0 0 0 0
|
||||||
|
, hover = Css.rgba 0 0 0 0
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Css.rgba 0 0 0 0
|
||||||
|
}
|
||||||
|
, [ Css.hover
|
||||||
|
[ Css.textDecoration Css.underline
|
||||||
|
, Css.disabled [ Css.textDecoration Css.none ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
DangerColors ->
|
||||||
|
( { background = Colors.red
|
||||||
|
, hover = Colors.redDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.redDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
PremiumColors ->
|
||||||
|
( { background = Colors.yellow
|
||||||
|
, hover = Colors.ochre
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.ochre
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
InactiveColors ->
|
||||||
|
( { background = Colors.gray92
|
||||||
|
, hover = Colors.gray92
|
||||||
|
, text = Colors.gray45
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.gray92
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
LoadingColors ->
|
||||||
|
( { background = Colors.glacier
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.glacier
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SuccessColors ->
|
||||||
|
( { background = Colors.greenDark
|
||||||
|
, hover = Colors.greenDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.greenDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorColors ->
|
||||||
|
( { background = Colors.purple
|
||||||
|
, hover = Colors.purple
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.purple
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
in
|
||||||
|
[ Css.batch additionalStyles
|
||||||
|
, Css.color config.text
|
||||||
|
, Css.backgroundColor config.background
|
||||||
|
, Css.fontWeight (Css.int 700)
|
||||||
|
, Css.textAlign Css.center
|
||||||
|
, case config.border of
|
||||||
|
Nothing ->
|
||||||
|
Css.borderStyle Css.none
|
||||||
|
|
||||||
|
Just color ->
|
||||||
|
Css.batch
|
||||||
|
[ Css.borderColor color
|
||||||
|
, Css.borderStyle Css.solid
|
||||||
|
]
|
||||||
|
, Css.borderBottomStyle Css.solid
|
||||||
|
, Css.borderBottomColor config.shadow
|
||||||
|
, Css.fontStyle Css.normal
|
||||||
|
, Css.hover
|
||||||
|
[ Css.color config.text
|
||||||
|
, Css.backgroundColor config.hover
|
||||||
|
, Css.disabled [ Css.backgroundColor config.background ]
|
||||||
|
]
|
||||||
|
, Css.visited [ Css.color config.text ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
type ElementType
|
||||||
|
= Anchor
|
||||||
|
| Button
|
||||||
|
|
||||||
|
|
||||||
|
sizeStyle : ButtonSize -> Maybe Int -> ElementType -> List Style
|
||||||
|
sizeStyle size width elementType =
|
||||||
|
let
|
||||||
|
config =
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
{ fontSize = 15
|
||||||
|
, height = 36
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 2
|
||||||
|
, minWidth = 75
|
||||||
|
}
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
{ fontSize = 17
|
||||||
|
, height = 45
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 3
|
||||||
|
, minWidth = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
{ fontSize = 20
|
||||||
|
, height = 56
|
||||||
|
, imageHeight = 20
|
||||||
|
, shadowHeight = 4
|
||||||
|
, minWidth = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
widthAttributes =
|
||||||
|
case width of
|
||||||
|
Just pxWidth ->
|
||||||
|
[ Css.maxWidth (Css.pct 100)
|
||||||
|
, Css.width (Css.px <| toFloat pxWidth)
|
||||||
|
]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
[ Css.padding2 Css.zero (Css.px 16)
|
||||||
|
, Css.minWidth (Css.px config.minWidth)
|
||||||
|
]
|
||||||
|
|
||||||
|
lineHeightPx =
|
||||||
|
case elementType of
|
||||||
|
Anchor ->
|
||||||
|
config.height
|
||||||
|
|
||||||
|
Button ->
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
15
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
19
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
22
|
||||||
|
in
|
||||||
|
[ Css.fontSize (Css.px config.fontSize)
|
||||||
|
, Css.borderRadius (Css.px 8)
|
||||||
|
, Css.height (Css.px config.height)
|
||||||
|
, Css.lineHeight (Css.px lineHeightPx)
|
||||||
|
, Css.boxSizing Css.borderBox
|
||||||
|
, Css.borderWidth (Css.px 1)
|
||||||
|
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||||
|
, Css.batch widthAttributes
|
||||||
|
, Css.Global.descendants
|
||||||
|
[ Css.Global.img
|
||||||
|
[ Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.important <| Css.width Css.auto
|
||||||
|
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||||
|
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
styledName : String -> String
|
||||||
|
styledName suffix =
|
||||||
|
"Nri-Ui-Button-V3-" ++ suffix
|
811
src/Nri/Ui/Button/V4.elm
Normal file
811
src/Nri/Ui/Button/V4.elm
Normal file
@ -0,0 +1,811 @@
|
|||||||
|
module Nri.Ui.Button.V4 exposing
|
||||||
|
( ButtonSize(..), ButtonWidth(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||||
|
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
# Changes from V3:
|
||||||
|
|
||||||
|
- Adds `ButtonWidth`.
|
||||||
|
- Button now grows vertically to fit content.
|
||||||
|
To limit the height use attributes on its container or consider truncating content before rendering.
|
||||||
|
|
||||||
|
|
||||||
|
# About:
|
||||||
|
|
||||||
|
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||||
|
action on the current page should be HTML `<button>` elements and are created here
|
||||||
|
with `*Button` functions. Buttons that take the user to a new page should be
|
||||||
|
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||||
|
should be able to use the same CSS class in all cases.
|
||||||
|
|
||||||
|
There will generally be a `*Button` and `*Link` version of each button style.
|
||||||
|
(These will be created as they are needed.)
|
||||||
|
|
||||||
|
In general a button should never truncate or obscure its contents. This could
|
||||||
|
make it difficult or impossible for a student or teacher to use the site, so in
|
||||||
|
general choose buttons that grow to fit their contents. It is better to risk
|
||||||
|
weird layout than to block users. Might this be a golden rule? Of course there
|
||||||
|
may be exceptions, for example if button content is supplied by an end-user.
|
||||||
|
|
||||||
|
|
||||||
|
## Common configs
|
||||||
|
|
||||||
|
@docs ButtonSize, ButtonWidth, ButtonStyle, ButtonState, ButtonContent
|
||||||
|
|
||||||
|
|
||||||
|
## `<button>` Buttons
|
||||||
|
|
||||||
|
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
|
||||||
|
|
||||||
|
## `<a>` Buttons
|
||||||
|
|
||||||
|
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||||
|
import Accessibility.Styled.Role as Role
|
||||||
|
import Accessibility.Styled.Widget as Widget
|
||||||
|
import Css exposing (Style)
|
||||||
|
import Css.Global
|
||||||
|
import EventExtras.Styled as EventExtras
|
||||||
|
import Html.Styled as Styled
|
||||||
|
import Html.Styled.Attributes as Attributes
|
||||||
|
import Html.Styled.Events as Events
|
||||||
|
import Json.Decode
|
||||||
|
import Markdown.Block
|
||||||
|
import Markdown.Inline
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||||
|
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1
|
||||||
|
import Nri.Ui.Icon.V3 as Icon exposing (IconType)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Sizes for buttons and links that have button classes
|
||||||
|
-}
|
||||||
|
type ButtonSize
|
||||||
|
= Small
|
||||||
|
| Medium
|
||||||
|
| Large
|
||||||
|
|
||||||
|
|
||||||
|
{-| Width sizing behavior for buttons.
|
||||||
|
|
||||||
|
`WidthExact Int` defines a size in `px` for the button's total width, and
|
||||||
|
`WidthUnbounded` leaves the maxiumum width unbounded (there is a minimum width).
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonWidth
|
||||||
|
= WidthExact Int
|
||||||
|
| WidthUnbounded
|
||||||
|
|
||||||
|
|
||||||
|
{-| Styleguide-approved styles for your buttons!
|
||||||
|
|
||||||
|
Note on borderless buttons:
|
||||||
|
A borderless button that performs an action on the current page
|
||||||
|
This button is intended to look like a link.
|
||||||
|
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonStyle
|
||||||
|
= Primary
|
||||||
|
| Secondary
|
||||||
|
| Borderless
|
||||||
|
| Danger
|
||||||
|
| Premium
|
||||||
|
|
||||||
|
|
||||||
|
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||||
|
|
||||||
|
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||||
|
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||||
|
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||||
|
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||||
|
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||||
|
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonState
|
||||||
|
= Enabled
|
||||||
|
| Unfulfilled
|
||||||
|
| Disabled
|
||||||
|
| Error
|
||||||
|
| Loading
|
||||||
|
| Success
|
||||||
|
|
||||||
|
|
||||||
|
{-| The part of a button that remains constant through different button states
|
||||||
|
-}
|
||||||
|
type alias ButtonConfig msg =
|
||||||
|
{ onClick : msg
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||||
|
state may have a different label than a button in the "Error" state
|
||||||
|
-}
|
||||||
|
type alias ButtonContent =
|
||||||
|
{ label : String
|
||||||
|
, state : ButtonState
|
||||||
|
, icon : Maybe IconType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delightful button which can trigger an effect when clicked!
|
||||||
|
|
||||||
|
This button will trigger the passed-in message if the button state is:
|
||||||
|
|
||||||
|
- Enabled
|
||||||
|
- Unfulfilled
|
||||||
|
|
||||||
|
This button will be Disabled if the button state is:
|
||||||
|
|
||||||
|
- Disabled
|
||||||
|
- Error
|
||||||
|
- Loading
|
||||||
|
- Success
|
||||||
|
|
||||||
|
-}
|
||||||
|
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
button config content =
|
||||||
|
customButton [] config content
|
||||||
|
|
||||||
|
|
||||||
|
{-| Exactly the same as button but you can pass in a list of attributes
|
||||||
|
-}
|
||||||
|
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
customButton attributes config content =
|
||||||
|
let
|
||||||
|
buttonStyle_ =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
styleToColorPalette config.style
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
ErrorColors
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
LoadingColors
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
SuccessColors
|
||||||
|
|
||||||
|
disabled =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
True
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "customButton")
|
||||||
|
(buttonStyles config.size config.width buttonStyle_ Button)
|
||||||
|
([ Events.onClick config.onClick
|
||||||
|
, Attributes.disabled disabled
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
++ attributes
|
||||||
|
)
|
||||||
|
(viewLabel content.icon content.label)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- COPY TO CLIPBOARD BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Config for copyToClipboard
|
||||||
|
-}
|
||||||
|
type alias CopyToClipboardConfig =
|
||||||
|
{ size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, copyText : String
|
||||||
|
, buttonLabel : String
|
||||||
|
, withIcon : Bool
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||||
|
You will need to hook this up to clipboard.js
|
||||||
|
-}
|
||||||
|
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||||
|
copyToClipboard assets config =
|
||||||
|
let
|
||||||
|
maybeIcon =
|
||||||
|
if config.withIcon then
|
||||||
|
Just (Icon.copy assets)
|
||||||
|
|
||||||
|
else
|
||||||
|
Nothing
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "copyToClipboard")
|
||||||
|
(buttonStyles config.size config.width (styleToColorPalette config.style) Button)
|
||||||
|
[ Widget.label "Copy URL to clipboard"
|
||||||
|
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||||
|
]
|
||||||
|
(viewLabel maybeIcon config.buttonLabel)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- DELETE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
type alias DeleteButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onClick : msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delete button (blue X)
|
||||||
|
-}
|
||||||
|
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||||
|
delete assets config =
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "delete")
|
||||||
|
[ Css.display Css.inlineBlock
|
||||||
|
, Css.backgroundRepeat Css.noRepeat
|
||||||
|
, Css.backgroundColor Css.transparent
|
||||||
|
, Css.backgroundPosition Css.center
|
||||||
|
, Css.backgroundSize Css.contain
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.width (Css.px 15)
|
||||||
|
, Css.height (Css.px 15)
|
||||||
|
, Css.padding Css.zero
|
||||||
|
, Css.margin2 Css.zero (Css.px 6)
|
||||||
|
, Css.cursor Css.pointer
|
||||||
|
, Css.color Colors.azure
|
||||||
|
]
|
||||||
|
[ Events.onClick config.onClick
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
Widget.label config.label
|
||||||
|
]
|
||||||
|
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- TOGGLE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Buttons can be toggled into a pressed state and back again.
|
||||||
|
-}
|
||||||
|
type alias ToggleButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onSelect : msg
|
||||||
|
, onDeselect : msg
|
||||||
|
, pressed : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||||
|
toggleButton config =
|
||||||
|
let
|
||||||
|
toggledStyles =
|
||||||
|
if config.pressed then
|
||||||
|
[ Css.color Colors.gray20
|
||||||
|
, Css.backgroundColor Colors.glacier
|
||||||
|
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||||
|
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||||
|
, Css.fontWeight Css.bold
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "toggleButton")
|
||||||
|
(buttonStyles Medium WidthUnbounded SecondaryColors Button
|
||||||
|
++ toggledStyles
|
||||||
|
)
|
||||||
|
[ Events.onClick
|
||||||
|
(if config.pressed then
|
||||||
|
config.onDeselect
|
||||||
|
|
||||||
|
else
|
||||||
|
config.onSelect
|
||||||
|
)
|
||||||
|
, Widget.pressed <| Just config.pressed
|
||||||
|
|
||||||
|
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
, Role.button
|
||||||
|
|
||||||
|
-- Note: setting type: 'button' removes the default behavior of submit
|
||||||
|
-- equivalent to preventDefaultBehavior = false
|
||||||
|
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
(viewLabel Nothing config.label)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Inputs can be a clickable thing used in a form
|
||||||
|
-}
|
||||||
|
type alias InputConfig =
|
||||||
|
{ content : Html Never
|
||||||
|
, name : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, value : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- LINKS THAT LOOK LIKE BUTTONS
|
||||||
|
|
||||||
|
|
||||||
|
{-| Links are clickable things with a url.
|
||||||
|
|
||||||
|
NOTE: Links do not support two-line labels.
|
||||||
|
|
||||||
|
-}
|
||||||
|
type alias LinkConfig =
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, url : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url
|
||||||
|
-}
|
||||||
|
link : LinkConfig -> Html msg
|
||||||
|
link =
|
||||||
|
linkBase "link" [ Attributes.target "_self" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Use this link for routing within a single page app.
|
||||||
|
|
||||||
|
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||||
|
|
||||||
|
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkSpa :
|
||||||
|
(route -> String)
|
||||||
|
-> (route -> msg)
|
||||||
|
->
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
, route : route
|
||||||
|
}
|
||||||
|
-> Html msg
|
||||||
|
linkSpa toUrl toMsg config =
|
||||||
|
linkBase
|
||||||
|
"linkSpa"
|
||||||
|
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||||
|
]
|
||||||
|
{ label = config.label
|
||||||
|
, icon = config.icon
|
||||||
|
, size = config.size
|
||||||
|
, style = config.style
|
||||||
|
, width = config.width
|
||||||
|
, url = toUrl config.route
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url and have it open to an external site
|
||||||
|
-}
|
||||||
|
linkExternal : LinkConfig -> Html msg
|
||||||
|
linkExternal =
|
||||||
|
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||||
|
-}
|
||||||
|
linkWithMethod : String -> LinkConfig -> Html msg
|
||||||
|
linkWithMethod method =
|
||||||
|
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||||
|
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||||
|
-}
|
||||||
|
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkWithTracking"
|
||||||
|
[ Events.preventDefaultOn "click"
|
||||||
|
(Json.Decode.succeed ( onTrack, True ))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||||
|
|
||||||
|
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkExternalWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkExternalWithTracking"
|
||||||
|
[ Attributes.target "_blank"
|
||||||
|
, EventExtras.onClickForLinkWithHref onTrack
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Helper function for building links with an arbitrary number of Attributes
|
||||||
|
-}
|
||||||
|
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||||
|
linkBase linkFunctionName extraAttrs config =
|
||||||
|
Nri.Ui.styled Styled.a
|
||||||
|
(styledName linkFunctionName)
|
||||||
|
(Css.whiteSpace Css.noWrap
|
||||||
|
:: buttonStyles config.size config.width (styleToColorPalette config.style) Anchor
|
||||||
|
)
|
||||||
|
(Attributes.href config.url
|
||||||
|
:: extraAttrs
|
||||||
|
)
|
||||||
|
(viewLabel config.icon config.label)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HELPERS
|
||||||
|
|
||||||
|
|
||||||
|
type ColorPalette
|
||||||
|
= PrimaryColors
|
||||||
|
| SecondaryColors
|
||||||
|
| BorderlessColors
|
||||||
|
| DangerColors
|
||||||
|
| PremiumColors
|
||||||
|
| InactiveColors
|
||||||
|
| LoadingColors
|
||||||
|
| SuccessColors
|
||||||
|
| ErrorColors
|
||||||
|
|
||||||
|
|
||||||
|
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||||
|
styleToColorPalette style =
|
||||||
|
case style of
|
||||||
|
Primary ->
|
||||||
|
PrimaryColors
|
||||||
|
|
||||||
|
Secondary ->
|
||||||
|
SecondaryColors
|
||||||
|
|
||||||
|
Borderless ->
|
||||||
|
BorderlessColors
|
||||||
|
|
||||||
|
Danger ->
|
||||||
|
DangerColors
|
||||||
|
|
||||||
|
Premium ->
|
||||||
|
PremiumColors
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyles : ButtonSize -> ButtonWidth -> ColorPalette -> ElementType -> List Style
|
||||||
|
buttonStyles size width colorPalette elementType =
|
||||||
|
List.concat
|
||||||
|
[ buttonStyle
|
||||||
|
, colorStyle colorPalette
|
||||||
|
, sizeStyle size width elementType
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewLabel : Maybe IconType -> String -> List (Html msg)
|
||||||
|
viewLabel icn label =
|
||||||
|
case icn of
|
||||||
|
Nothing ->
|
||||||
|
renderMarkdown label
|
||||||
|
|
||||||
|
Just iconType ->
|
||||||
|
[ Html.span [] (Icon.decorativeIcon iconType :: renderMarkdown label) ]
|
||||||
|
|
||||||
|
|
||||||
|
renderMarkdown : String -> List (Html msg)
|
||||||
|
renderMarkdown markdown =
|
||||||
|
case Markdown.Block.parse Nothing markdown of
|
||||||
|
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||||
|
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||||
|
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[ Html.text markdown ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- STYLES
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyle : List Style
|
||||||
|
buttonStyle =
|
||||||
|
[ Css.cursor Css.pointer
|
||||||
|
, Css.display Css.inlineBlock
|
||||||
|
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||||
|
Nri.Ui.Fonts.V1.baseFont
|
||||||
|
, Css.textOverflow Css.ellipsis
|
||||||
|
, Css.overflow Css.hidden
|
||||||
|
, Css.textDecoration Css.none
|
||||||
|
, Css.backgroundImage Css.none
|
||||||
|
, Css.textShadow Css.none
|
||||||
|
, Css.property "transition" "all 0.2s"
|
||||||
|
, Css.boxShadow Css.none
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.marginBottom Css.zero
|
||||||
|
, Css.hover [ Css.textDecoration Css.none ]
|
||||||
|
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
colorStyle : ColorPalette -> List Style
|
||||||
|
colorStyle colorPalette =
|
||||||
|
let
|
||||||
|
( config, additionalStyles ) =
|
||||||
|
case colorPalette of
|
||||||
|
PrimaryColors ->
|
||||||
|
( { background = Colors.azure
|
||||||
|
, hover = Colors.azureDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.azureDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SecondaryColors ->
|
||||||
|
( { background = Colors.white
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Just <| Colors.azure
|
||||||
|
, shadow = Colors.azure
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
BorderlessColors ->
|
||||||
|
( { background = Css.rgba 0 0 0 0
|
||||||
|
, hover = Css.rgba 0 0 0 0
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Css.rgba 0 0 0 0
|
||||||
|
}
|
||||||
|
, [ Css.hover
|
||||||
|
[ Css.textDecoration Css.underline
|
||||||
|
, Css.disabled [ Css.textDecoration Css.none ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
DangerColors ->
|
||||||
|
( { background = Colors.red
|
||||||
|
, hover = Colors.redDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.redDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
PremiumColors ->
|
||||||
|
( { background = Colors.yellow
|
||||||
|
, hover = Colors.ochre
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.ochre
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
InactiveColors ->
|
||||||
|
( { background = Colors.gray92
|
||||||
|
, hover = Colors.gray92
|
||||||
|
, text = Colors.gray45
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.gray92
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
LoadingColors ->
|
||||||
|
( { background = Colors.glacier
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.glacier
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SuccessColors ->
|
||||||
|
( { background = Colors.greenDark
|
||||||
|
, hover = Colors.greenDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.greenDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorColors ->
|
||||||
|
( { background = Colors.purple
|
||||||
|
, hover = Colors.purple
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.purple
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
in
|
||||||
|
[ Css.batch additionalStyles
|
||||||
|
, Css.color config.text
|
||||||
|
, Css.backgroundColor config.background
|
||||||
|
, Css.fontWeight (Css.int 700)
|
||||||
|
, Css.textAlign Css.center
|
||||||
|
, case config.border of
|
||||||
|
Nothing ->
|
||||||
|
Css.borderStyle Css.none
|
||||||
|
|
||||||
|
Just color ->
|
||||||
|
Css.batch
|
||||||
|
[ Css.borderColor color
|
||||||
|
, Css.borderStyle Css.solid
|
||||||
|
]
|
||||||
|
, Css.borderBottomStyle Css.solid
|
||||||
|
, Css.borderBottomColor config.shadow
|
||||||
|
, Css.fontStyle Css.normal
|
||||||
|
, Css.hover
|
||||||
|
[ Css.color config.text
|
||||||
|
, Css.backgroundColor config.hover
|
||||||
|
, Css.disabled [ Css.backgroundColor config.background ]
|
||||||
|
]
|
||||||
|
, Css.visited [ Css.color config.text ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
type ElementType
|
||||||
|
= Anchor
|
||||||
|
| Button
|
||||||
|
|
||||||
|
|
||||||
|
sizeStyle : ButtonSize -> ButtonWidth -> ElementType -> List Style
|
||||||
|
sizeStyle size width elementType =
|
||||||
|
let
|
||||||
|
config =
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
{ fontSize = 15
|
||||||
|
, height = 36
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 2
|
||||||
|
, minWidth = 75
|
||||||
|
}
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
{ fontSize = 17
|
||||||
|
, height = 45
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 3
|
||||||
|
, minWidth = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
{ fontSize = 20
|
||||||
|
, height = 56
|
||||||
|
, imageHeight = 20
|
||||||
|
, shadowHeight = 4
|
||||||
|
, minWidth = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
sizingAttributes =
|
||||||
|
case elementType of
|
||||||
|
Button ->
|
||||||
|
let
|
||||||
|
verticalPaddingPx =
|
||||||
|
4
|
||||||
|
in
|
||||||
|
[ Css.minHeight (Css.px config.height)
|
||||||
|
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||||
|
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||||
|
]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
widthAttributes =
|
||||||
|
case width of
|
||||||
|
WidthExact pxWidth ->
|
||||||
|
[ Css.maxWidth (Css.pct 100)
|
||||||
|
, Css.width (Css.px <| toFloat pxWidth)
|
||||||
|
]
|
||||||
|
|
||||||
|
WidthUnbounded ->
|
||||||
|
[ Css.paddingLeft (Css.px 16)
|
||||||
|
, Css.paddingRight (Css.px 16)
|
||||||
|
, Css.minWidth (Css.px config.minWidth)
|
||||||
|
]
|
||||||
|
|
||||||
|
lineHeightPx =
|
||||||
|
case elementType of
|
||||||
|
Anchor ->
|
||||||
|
config.height
|
||||||
|
|
||||||
|
Button ->
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
15
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
19
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
22
|
||||||
|
in
|
||||||
|
[ Css.fontSize (Css.px config.fontSize)
|
||||||
|
, Css.borderRadius (Css.px 8)
|
||||||
|
, Css.lineHeight (Css.px lineHeightPx)
|
||||||
|
, Css.boxSizing Css.borderBox
|
||||||
|
, Css.borderWidth (Css.px 1)
|
||||||
|
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||||
|
, Css.batch sizingAttributes
|
||||||
|
, Css.batch widthAttributes
|
||||||
|
, Css.Global.descendants
|
||||||
|
[ Css.Global.img
|
||||||
|
[ Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.important <| Css.width Css.auto
|
||||||
|
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||||
|
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
styledName : String -> String
|
||||||
|
styledName suffix =
|
||||||
|
"Nri-Ui-Button-V4-" ++ suffix
|
810
src/Nri/Ui/Button/V6.elm
Normal file
810
src/Nri/Ui/Button/V6.elm
Normal file
@ -0,0 +1,810 @@
|
|||||||
|
module Nri.Ui.Button.V6 exposing
|
||||||
|
( ButtonSize(..), ButtonWidth(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||||
|
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
# Changes from V5:
|
||||||
|
|
||||||
|
- Update version of Nri.Ui.Icon to V4
|
||||||
|
|
||||||
|
|
||||||
|
# About:
|
||||||
|
|
||||||
|
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||||
|
action on the current page should be HTML `<button>` elements and are created here
|
||||||
|
with `*Button` functions. Buttons that take the user to a new page should be
|
||||||
|
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||||
|
should be able to use the same CSS class in all cases.
|
||||||
|
|
||||||
|
There will generally be a `*Button` and `*Link` version of each button style.
|
||||||
|
(These will be created as they are needed.)
|
||||||
|
|
||||||
|
In general a button should never truncate or obscure its contents. This could
|
||||||
|
make it difficult or impossible for a student or teacher to use the site, so in
|
||||||
|
general choose buttons that grow to fit their contents. It is better to risk
|
||||||
|
weird layout than to block users. Might this be a golden rule? Of course there
|
||||||
|
may be exceptions, for example if button content is supplied by an end-user.
|
||||||
|
|
||||||
|
|
||||||
|
## Common configs
|
||||||
|
|
||||||
|
@docs ButtonSize, ButtonWidth, ButtonStyle, ButtonState, ButtonContent
|
||||||
|
|
||||||
|
|
||||||
|
## `<button>` Buttons
|
||||||
|
|
||||||
|
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
|
||||||
|
|
||||||
|
## `<a>` Buttons
|
||||||
|
|
||||||
|
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||||
|
import Accessibility.Styled.Role as Role
|
||||||
|
import Accessibility.Styled.Widget as Widget
|
||||||
|
import Css exposing (Style)
|
||||||
|
import Css.Global
|
||||||
|
import EventExtras.Styled as EventExtras
|
||||||
|
import Html.Styled as Styled
|
||||||
|
import Html.Styled.Attributes as Attributes
|
||||||
|
import Html.Styled.Events as Events
|
||||||
|
import Json.Decode
|
||||||
|
import Markdown.Block
|
||||||
|
import Markdown.Inline
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||||
|
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1
|
||||||
|
import Nri.Ui.Icon.V4 as Icon exposing (IconType)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Sizes for buttons and links that have button classes
|
||||||
|
-}
|
||||||
|
type ButtonSize
|
||||||
|
= Small
|
||||||
|
| Medium
|
||||||
|
| Large
|
||||||
|
|
||||||
|
|
||||||
|
{-| Width sizing behavior for buttons.
|
||||||
|
|
||||||
|
`WidthExact Int` defines a size in `px` for the button's total width, and
|
||||||
|
`WidthUnbounded` leaves the maxiumum width unbounded (there is a minimum width).
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonWidth
|
||||||
|
= WidthExact Int
|
||||||
|
| WidthUnbounded
|
||||||
|
|
||||||
|
|
||||||
|
{-| Styleguide-approved styles for your buttons!
|
||||||
|
|
||||||
|
Note on borderless buttons:
|
||||||
|
A borderless button that performs an action on the current page
|
||||||
|
This button is intended to look like a link.
|
||||||
|
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonStyle
|
||||||
|
= Primary
|
||||||
|
| Secondary
|
||||||
|
| Borderless
|
||||||
|
| Danger
|
||||||
|
| Premium
|
||||||
|
|
||||||
|
|
||||||
|
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||||
|
|
||||||
|
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||||
|
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||||
|
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||||
|
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||||
|
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||||
|
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonState
|
||||||
|
= Enabled
|
||||||
|
| Unfulfilled
|
||||||
|
| Disabled
|
||||||
|
| Error
|
||||||
|
| Loading
|
||||||
|
| Success
|
||||||
|
|
||||||
|
|
||||||
|
{-| The part of a button that remains constant through different button states
|
||||||
|
-}
|
||||||
|
type alias ButtonConfig msg =
|
||||||
|
{ onClick : msg
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||||
|
state may have a different label than a button in the "Error" state
|
||||||
|
-}
|
||||||
|
type alias ButtonContent =
|
||||||
|
{ label : String
|
||||||
|
, state : ButtonState
|
||||||
|
, icon : Maybe IconType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delightful button which can trigger an effect when clicked!
|
||||||
|
|
||||||
|
This button will trigger the passed-in message if the button state is:
|
||||||
|
|
||||||
|
- Enabled
|
||||||
|
- Unfulfilled
|
||||||
|
|
||||||
|
This button will be Disabled if the button state is:
|
||||||
|
|
||||||
|
- Disabled
|
||||||
|
- Error
|
||||||
|
- Loading
|
||||||
|
- Success
|
||||||
|
|
||||||
|
-}
|
||||||
|
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
button config content =
|
||||||
|
customButton [] config content
|
||||||
|
|
||||||
|
|
||||||
|
{-| Exactly the same as button but you can pass in a list of attributes
|
||||||
|
-}
|
||||||
|
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
customButton attributes config content =
|
||||||
|
let
|
||||||
|
buttonStyle_ =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
styleToColorPalette config.style
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
ErrorColors
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
LoadingColors
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
SuccessColors
|
||||||
|
|
||||||
|
disabled =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
True
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "customButton")
|
||||||
|
[ buttonStyles config.size config.width buttonStyle_ ]
|
||||||
|
([ Events.onClick config.onClick
|
||||||
|
, Attributes.disabled disabled
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
++ attributes
|
||||||
|
)
|
||||||
|
[ viewLabel content.icon content.label ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- COPY TO CLIPBOARD BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Config for copyToClipboard
|
||||||
|
-}
|
||||||
|
type alias CopyToClipboardConfig =
|
||||||
|
{ size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, copyText : String
|
||||||
|
, buttonLabel : String
|
||||||
|
, withIcon : Bool
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||||
|
You will need to hook this up to clipboard.js
|
||||||
|
-}
|
||||||
|
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||||
|
copyToClipboard assets config =
|
||||||
|
let
|
||||||
|
maybeIcon =
|
||||||
|
if config.withIcon then
|
||||||
|
Just (Icon.copy assets)
|
||||||
|
|
||||||
|
else
|
||||||
|
Nothing
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "copyToClipboard")
|
||||||
|
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||||
|
[ Widget.label "Copy URL to clipboard"
|
||||||
|
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||||
|
]
|
||||||
|
[ viewLabel maybeIcon config.buttonLabel ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- DELETE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
type alias DeleteButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onClick : msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delete button (blue X)
|
||||||
|
-}
|
||||||
|
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||||
|
delete assets config =
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "delete")
|
||||||
|
[ Css.display Css.inlineBlock
|
||||||
|
, Css.backgroundRepeat Css.noRepeat
|
||||||
|
, Css.backgroundColor Css.transparent
|
||||||
|
, Css.backgroundPosition Css.center
|
||||||
|
, Css.backgroundSize Css.contain
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.width (Css.px 15)
|
||||||
|
, Css.height (Css.px 15)
|
||||||
|
, Css.padding Css.zero
|
||||||
|
, Css.margin2 Css.zero (Css.px 6)
|
||||||
|
, Css.cursor Css.pointer
|
||||||
|
, Css.color Colors.azure
|
||||||
|
]
|
||||||
|
[ Events.onClick config.onClick
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
Widget.label config.label
|
||||||
|
]
|
||||||
|
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- TOGGLE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Buttons can be toggled into a pressed state and back again.
|
||||||
|
-}
|
||||||
|
type alias ToggleButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onSelect : msg
|
||||||
|
, onDeselect : msg
|
||||||
|
, pressed : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||||
|
toggleButton config =
|
||||||
|
let
|
||||||
|
toggledStyles =
|
||||||
|
if config.pressed then
|
||||||
|
Css.batch
|
||||||
|
[ Css.color Colors.gray20
|
||||||
|
, Css.backgroundColor Colors.glacier
|
||||||
|
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||||
|
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||||
|
, Css.fontWeight Css.bold
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
Css.batch
|
||||||
|
[]
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "toggleButton")
|
||||||
|
[ buttonStyles Medium WidthUnbounded SecondaryColors
|
||||||
|
, toggledStyles
|
||||||
|
]
|
||||||
|
[ Events.onClick
|
||||||
|
(if config.pressed then
|
||||||
|
config.onDeselect
|
||||||
|
|
||||||
|
else
|
||||||
|
config.onSelect
|
||||||
|
)
|
||||||
|
, Widget.pressed <| Just config.pressed
|
||||||
|
|
||||||
|
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
, Role.button
|
||||||
|
|
||||||
|
-- Note: setting type: 'button' removes the default behavior of submit
|
||||||
|
-- equivalent to preventDefaultBehavior = false
|
||||||
|
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
[ viewLabel Nothing config.label ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Inputs can be a clickable thing used in a form
|
||||||
|
-}
|
||||||
|
type alias InputConfig =
|
||||||
|
{ content : Html Never
|
||||||
|
, name : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, value : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- LINKS THAT LOOK LIKE BUTTONS
|
||||||
|
|
||||||
|
|
||||||
|
{-| Links are clickable things with a url.
|
||||||
|
|
||||||
|
NOTE: Links do not support two-line labels.
|
||||||
|
|
||||||
|
-}
|
||||||
|
type alias LinkConfig =
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, url : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url
|
||||||
|
-}
|
||||||
|
link : LinkConfig -> Html msg
|
||||||
|
link =
|
||||||
|
linkBase "link" [ Attributes.target "_self" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Use this link for routing within a single page app.
|
||||||
|
|
||||||
|
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||||
|
|
||||||
|
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkSpa :
|
||||||
|
(route -> String)
|
||||||
|
-> (route -> msg)
|
||||||
|
->
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
, route : route
|
||||||
|
}
|
||||||
|
-> Html msg
|
||||||
|
linkSpa toUrl toMsg config =
|
||||||
|
linkBase
|
||||||
|
"linkSpa"
|
||||||
|
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||||
|
]
|
||||||
|
{ label = config.label
|
||||||
|
, icon = config.icon
|
||||||
|
, size = config.size
|
||||||
|
, style = config.style
|
||||||
|
, width = config.width
|
||||||
|
, url = toUrl config.route
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url and have it open to an external site
|
||||||
|
-}
|
||||||
|
linkExternal : LinkConfig -> Html msg
|
||||||
|
linkExternal =
|
||||||
|
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||||
|
-}
|
||||||
|
linkWithMethod : String -> LinkConfig -> Html msg
|
||||||
|
linkWithMethod method =
|
||||||
|
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||||
|
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||||
|
-}
|
||||||
|
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkWithTracking"
|
||||||
|
[ Events.preventDefaultOn "click"
|
||||||
|
(Json.Decode.succeed ( onTrack, True ))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||||
|
|
||||||
|
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkExternalWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkExternalWithTracking"
|
||||||
|
[ Attributes.target "_blank"
|
||||||
|
, EventExtras.onClickForLinkWithHref onTrack
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Helper function for building links with an arbitrary number of Attributes
|
||||||
|
-}
|
||||||
|
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||||
|
linkBase linkFunctionName extraAttrs config =
|
||||||
|
Nri.Ui.styled Styled.a
|
||||||
|
(styledName linkFunctionName)
|
||||||
|
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||||
|
(Attributes.href config.url
|
||||||
|
:: extraAttrs
|
||||||
|
)
|
||||||
|
[ viewLabel config.icon config.label ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HELPERS
|
||||||
|
|
||||||
|
|
||||||
|
type ColorPalette
|
||||||
|
= PrimaryColors
|
||||||
|
| SecondaryColors
|
||||||
|
| BorderlessColors
|
||||||
|
| DangerColors
|
||||||
|
| PremiumColors
|
||||||
|
| InactiveColors
|
||||||
|
| LoadingColors
|
||||||
|
| SuccessColors
|
||||||
|
| ErrorColors
|
||||||
|
|
||||||
|
|
||||||
|
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||||
|
styleToColorPalette style =
|
||||||
|
case style of
|
||||||
|
Primary ->
|
||||||
|
PrimaryColors
|
||||||
|
|
||||||
|
Secondary ->
|
||||||
|
SecondaryColors
|
||||||
|
|
||||||
|
Borderless ->
|
||||||
|
BorderlessColors
|
||||||
|
|
||||||
|
Danger ->
|
||||||
|
DangerColors
|
||||||
|
|
||||||
|
Premium ->
|
||||||
|
PremiumColors
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyles : ButtonSize -> ButtonWidth -> ColorPalette -> Style
|
||||||
|
buttonStyles size width colorPalette =
|
||||||
|
Css.batch
|
||||||
|
[ buttonStyle
|
||||||
|
, colorStyle colorPalette
|
||||||
|
, sizeStyle size width
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewLabel : Maybe IconType -> String -> Html msg
|
||||||
|
viewLabel icn label =
|
||||||
|
Nri.Ui.styled Html.span
|
||||||
|
"button-label-span"
|
||||||
|
[ Css.overflow Css.hidden -- Keep scrollbars out of our button
|
||||||
|
, Css.overflowWrap Css.breakWord -- Ensure that words that exceed the button width break instead of disappearing
|
||||||
|
, Css.padding2 (Css.px 2) Css.zero -- Without a bit of bottom padding, text that extends below the baseline, like "g" gets cut off
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
(case icn of
|
||||||
|
Nothing ->
|
||||||
|
renderMarkdown label
|
||||||
|
|
||||||
|
Just iconType ->
|
||||||
|
Icon.decorativeIcon iconType :: renderMarkdown label
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
renderMarkdown : String -> List (Html msg)
|
||||||
|
renderMarkdown markdown =
|
||||||
|
case Markdown.Block.parse Nothing markdown of
|
||||||
|
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||||
|
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||||
|
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[ Html.text markdown ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- STYLES
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyle : Style
|
||||||
|
buttonStyle =
|
||||||
|
Css.batch
|
||||||
|
[ Css.cursor Css.pointer
|
||||||
|
, Css.display Css.inlineBlock
|
||||||
|
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||||
|
Nri.Ui.Fonts.V1.baseFont
|
||||||
|
, Css.textOverflow Css.ellipsis
|
||||||
|
, Css.overflow Css.hidden
|
||||||
|
, Css.textDecoration Css.none
|
||||||
|
, Css.backgroundImage Css.none
|
||||||
|
, Css.textShadow Css.none
|
||||||
|
, Css.property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s"
|
||||||
|
, Css.boxShadow Css.none
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.marginBottom Css.zero
|
||||||
|
, Css.hover [ Css.textDecoration Css.none ]
|
||||||
|
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
colorStyle : ColorPalette -> Style
|
||||||
|
colorStyle colorPalette =
|
||||||
|
let
|
||||||
|
( config, additionalStyles ) =
|
||||||
|
case colorPalette of
|
||||||
|
PrimaryColors ->
|
||||||
|
( { background = Colors.azure
|
||||||
|
, hover = Colors.azureDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.azureDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SecondaryColors ->
|
||||||
|
( { background = Colors.white
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Just <| Colors.azure
|
||||||
|
, shadow = Colors.azure
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
BorderlessColors ->
|
||||||
|
( { background = Css.rgba 0 0 0 0
|
||||||
|
, hover = Css.rgba 0 0 0 0
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Css.rgba 0 0 0 0
|
||||||
|
}
|
||||||
|
, [ Css.hover
|
||||||
|
[ Css.textDecoration Css.underline
|
||||||
|
, Css.disabled [ Css.textDecoration Css.none ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
DangerColors ->
|
||||||
|
( { background = Colors.red
|
||||||
|
, hover = Colors.redDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.redDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
PremiumColors ->
|
||||||
|
( { background = Colors.yellow
|
||||||
|
, hover = Colors.ochre
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.ochre
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
InactiveColors ->
|
||||||
|
( { background = Colors.gray92
|
||||||
|
, hover = Colors.gray92
|
||||||
|
, text = Colors.gray45
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.gray92
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
LoadingColors ->
|
||||||
|
( { background = Colors.glacier
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.glacier
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SuccessColors ->
|
||||||
|
( { background = Colors.greenDark
|
||||||
|
, hover = Colors.greenDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.greenDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorColors ->
|
||||||
|
( { background = Colors.purple
|
||||||
|
, hover = Colors.purple
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.purple
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
in
|
||||||
|
Css.batch
|
||||||
|
[ Css.batch additionalStyles
|
||||||
|
, Css.color config.text
|
||||||
|
, Css.backgroundColor config.background
|
||||||
|
, Css.fontWeight (Css.int 700)
|
||||||
|
, Css.textAlign Css.center
|
||||||
|
, case config.border of
|
||||||
|
Nothing ->
|
||||||
|
Css.borderStyle Css.none
|
||||||
|
|
||||||
|
Just color ->
|
||||||
|
Css.batch
|
||||||
|
[ Css.borderColor color
|
||||||
|
, Css.borderStyle Css.solid
|
||||||
|
]
|
||||||
|
, Css.borderBottomStyle Css.solid
|
||||||
|
, Css.borderBottomColor config.shadow
|
||||||
|
, Css.fontStyle Css.normal
|
||||||
|
, Css.hover
|
||||||
|
[ Css.color config.text
|
||||||
|
, Css.backgroundColor config.hover
|
||||||
|
, Css.disabled [ Css.backgroundColor config.background ]
|
||||||
|
]
|
||||||
|
, Css.visited [ Css.color config.text ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
sizeStyle : ButtonSize -> ButtonWidth -> Style
|
||||||
|
sizeStyle size width =
|
||||||
|
let
|
||||||
|
config =
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
{ fontSize = 15
|
||||||
|
, height = 36
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 2
|
||||||
|
, minWidth = 75
|
||||||
|
}
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
{ fontSize = 17
|
||||||
|
, height = 45
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 3
|
||||||
|
, minWidth = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
{ fontSize = 20
|
||||||
|
, height = 56
|
||||||
|
, imageHeight = 20
|
||||||
|
, shadowHeight = 4
|
||||||
|
, minWidth = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
sizingAttributes =
|
||||||
|
let
|
||||||
|
verticalPaddingPx =
|
||||||
|
2
|
||||||
|
in
|
||||||
|
[ Css.minHeight (Css.px config.height)
|
||||||
|
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||||
|
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||||
|
]
|
||||||
|
|
||||||
|
widthAttributes =
|
||||||
|
case width of
|
||||||
|
WidthExact pxWidth ->
|
||||||
|
[ Css.maxWidth (Css.pct 100)
|
||||||
|
, Css.width (Css.px <| toFloat pxWidth)
|
||||||
|
, Css.paddingRight (Css.px 4)
|
||||||
|
, Css.paddingLeft (Css.px 4)
|
||||||
|
]
|
||||||
|
|
||||||
|
WidthUnbounded ->
|
||||||
|
[ Css.paddingLeft (Css.px 16)
|
||||||
|
, Css.paddingRight (Css.px 16)
|
||||||
|
, Css.minWidth (Css.px config.minWidth)
|
||||||
|
]
|
||||||
|
|
||||||
|
lineHeightPx =
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
15
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
19
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
22
|
||||||
|
in
|
||||||
|
Css.batch
|
||||||
|
[ Css.fontSize (Css.px config.fontSize)
|
||||||
|
, Css.borderRadius (Css.px 8)
|
||||||
|
, Css.lineHeight (Css.px lineHeightPx)
|
||||||
|
, Css.boxSizing Css.borderBox
|
||||||
|
, Css.borderWidth (Css.px 1)
|
||||||
|
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||||
|
, Css.batch sizingAttributes
|
||||||
|
, Css.batch widthAttributes
|
||||||
|
, Css.Global.descendants
|
||||||
|
[ Css.Global.img
|
||||||
|
[ Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.important <| Css.width Css.auto
|
||||||
|
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||||
|
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
styledName : String -> String
|
||||||
|
styledName suffix =
|
||||||
|
"Nri-Ui-Button-V6-" ++ suffix
|
818
src/Nri/Ui/Button/V7.elm
Normal file
818
src/Nri/Ui/Button/V7.elm
Normal file
@ -0,0 +1,818 @@
|
|||||||
|
module Nri.Ui.Button.V7 exposing
|
||||||
|
( ButtonSize(..), ButtonWidth(..), ButtonStyle(..), ButtonState(..), ButtonContent
|
||||||
|
, ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
, LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
# Changes from V6:
|
||||||
|
|
||||||
|
- WidthUnbounded button displays inlineFlex, so that it doesn't accidentally take up the entire width
|
||||||
|
- Add WidthFillContainer, for when we intend to take up the full container
|
||||||
|
|
||||||
|
|
||||||
|
# About:
|
||||||
|
|
||||||
|
Common NoRedInk buttons. For accessibility purposes, buttons that perform an
|
||||||
|
action on the current page should be HTML `<button>` elements and are created here
|
||||||
|
with `*Button` functions. Buttons that take the user to a new page should be
|
||||||
|
HTML `<a>` elements and are created here with `*Link` functions. Both versions
|
||||||
|
should be able to use the same CSS class in all cases.
|
||||||
|
|
||||||
|
There will generally be a `*Button` and `*Link` version of each button style.
|
||||||
|
(These will be created as they are needed.)
|
||||||
|
|
||||||
|
In general a button should never truncate or obscure its contents. This could
|
||||||
|
make it difficult or impossible for a student or teacher to use the site, so in
|
||||||
|
general choose buttons that grow to fit their contents. It is better to risk
|
||||||
|
weird layout than to block users. Might this be a golden rule? Of course there
|
||||||
|
may be exceptions, for example if button content is supplied by an end-user.
|
||||||
|
|
||||||
|
|
||||||
|
## Common configs
|
||||||
|
|
||||||
|
@docs ButtonSize, ButtonWidth, ButtonStyle, ButtonState, ButtonContent
|
||||||
|
|
||||||
|
|
||||||
|
## `<button>` Buttons
|
||||||
|
|
||||||
|
@docs ButtonConfig, button, customButton, delete, copyToClipboard, ToggleButtonConfig, toggleButton
|
||||||
|
|
||||||
|
|
||||||
|
## `<a>` Buttons
|
||||||
|
|
||||||
|
@docs LinkConfig, link, linkSpa, linkExternal, linkWithMethod, linkWithTracking, linkExternalWithTracking
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (Attribute, Html)
|
||||||
|
import Accessibility.Styled.Role as Role
|
||||||
|
import Accessibility.Styled.Widget as Widget
|
||||||
|
import Css exposing (Style)
|
||||||
|
import Css.Global
|
||||||
|
import EventExtras.Styled as EventExtras
|
||||||
|
import Html.Styled as Styled
|
||||||
|
import Html.Styled.Attributes as Attributes
|
||||||
|
import Html.Styled.Events as Events
|
||||||
|
import Json.Decode
|
||||||
|
import Markdown.Block
|
||||||
|
import Markdown.Inline
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.AssetPath as AssetPath exposing (Asset)
|
||||||
|
import Nri.Ui.Colors.Extra as ColorsExtra
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1
|
||||||
|
import Nri.Ui.Icon.V4 as Icon exposing (IconType)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Sizes for buttons and links that have button classes
|
||||||
|
-}
|
||||||
|
type ButtonSize
|
||||||
|
= Small
|
||||||
|
| Medium
|
||||||
|
| Large
|
||||||
|
|
||||||
|
|
||||||
|
{-| Width sizing behavior for buttons.
|
||||||
|
|
||||||
|
`WidthExact Int` defines a size in `px` for the button's total width, and
|
||||||
|
`WidthUnbounded` leaves the maxiumum width unbounded (there is a minimum width).
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonWidth
|
||||||
|
= WidthExact Int
|
||||||
|
| WidthUnbounded
|
||||||
|
| WidthFillContainer
|
||||||
|
|
||||||
|
|
||||||
|
{-| Styleguide-approved styles for your buttons!
|
||||||
|
|
||||||
|
Note on borderless buttons:
|
||||||
|
A borderless button that performs an action on the current page
|
||||||
|
This button is intended to look like a link.
|
||||||
|
Only use a borderless button when the clickable text in question follows the same layout/margin/padding as a bordered button
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonStyle
|
||||||
|
= Primary
|
||||||
|
| Secondary
|
||||||
|
| Borderless
|
||||||
|
| Danger
|
||||||
|
| Premium
|
||||||
|
|
||||||
|
|
||||||
|
{-| Describes the state of a button. Has consequences for appearance and disabled attribute.
|
||||||
|
|
||||||
|
- Enabled: An enabled button. Takes the appearance of ButtonStyle
|
||||||
|
- Unfulfilled: A button which appears with the InactiveColors palette but is not disabled.
|
||||||
|
- Disabled: A button which appears with the InactiveColors palette and is disabled.
|
||||||
|
- Error: A button which appears with the ErrorColors palette and is disabled.
|
||||||
|
- Loading: A button which appears with the LoadingColors palette and is disabled
|
||||||
|
- Success: A button which appears with the SuccessColors palette and is disabled
|
||||||
|
|
||||||
|
-}
|
||||||
|
type ButtonState
|
||||||
|
= Enabled
|
||||||
|
| Unfulfilled
|
||||||
|
| Disabled
|
||||||
|
| Error
|
||||||
|
| Loading
|
||||||
|
| Success
|
||||||
|
|
||||||
|
|
||||||
|
{-| The part of a button that remains constant through different button states
|
||||||
|
-}
|
||||||
|
type alias ButtonConfig msg =
|
||||||
|
{ onClick : msg
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| ButtonContent, often changes based on ButtonState. For example, a button in the "Success"
|
||||||
|
state may have a different label than a button in the "Error" state
|
||||||
|
-}
|
||||||
|
type alias ButtonContent =
|
||||||
|
{ label : String
|
||||||
|
, state : ButtonState
|
||||||
|
, icon : Maybe IconType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delightful button which can trigger an effect when clicked!
|
||||||
|
|
||||||
|
This button will trigger the passed-in message if the button state is:
|
||||||
|
|
||||||
|
- Enabled
|
||||||
|
- Unfulfilled
|
||||||
|
|
||||||
|
This button will be Disabled if the button state is:
|
||||||
|
|
||||||
|
- Disabled
|
||||||
|
- Error
|
||||||
|
- Loading
|
||||||
|
- Success
|
||||||
|
|
||||||
|
-}
|
||||||
|
button : ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
button config content =
|
||||||
|
customButton [] config content
|
||||||
|
|
||||||
|
|
||||||
|
{-| Exactly the same as button but you can pass in a list of attributes
|
||||||
|
-}
|
||||||
|
customButton : List (Attribute msg) -> ButtonConfig msg -> ButtonContent -> Html msg
|
||||||
|
customButton attributes config content =
|
||||||
|
let
|
||||||
|
buttonStyle_ =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
styleToColorPalette config.style
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
ErrorColors
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
InactiveColors
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
LoadingColors
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
SuccessColors
|
||||||
|
|
||||||
|
disabled =
|
||||||
|
case content.state of
|
||||||
|
Enabled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Disabled ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Error ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Unfulfilled ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Loading ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Success ->
|
||||||
|
True
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "customButton")
|
||||||
|
[ buttonStyles config.size config.width buttonStyle_ ]
|
||||||
|
([ Events.onClick config.onClick
|
||||||
|
, Attributes.disabled disabled
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
++ attributes
|
||||||
|
)
|
||||||
|
[ viewLabel content.icon content.label ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- COPY TO CLIPBOARD BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Config for copyToClipboard
|
||||||
|
-}
|
||||||
|
type alias CopyToClipboardConfig =
|
||||||
|
{ size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, copyText : String
|
||||||
|
, buttonLabel : String
|
||||||
|
, withIcon : Bool
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| See ui/src/Page/Teach/Courses/Assignments/index.coffee
|
||||||
|
You will need to hook this up to clipboard.js
|
||||||
|
-}
|
||||||
|
copyToClipboard : { r | teach_assignments_copyWhite_svg : Asset } -> CopyToClipboardConfig -> Html msg
|
||||||
|
copyToClipboard assets config =
|
||||||
|
let
|
||||||
|
maybeIcon =
|
||||||
|
if config.withIcon then
|
||||||
|
Just (Icon.copy assets)
|
||||||
|
|
||||||
|
else
|
||||||
|
Nothing
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "copyToClipboard")
|
||||||
|
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||||
|
[ Widget.label "Copy URL to clipboard"
|
||||||
|
, Attributes.attribute "data-clipboard-text" config.copyText
|
||||||
|
]
|
||||||
|
[ viewLabel maybeIcon config.buttonLabel ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- DELETE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
type alias DeleteButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onClick : msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A delete button (blue X)
|
||||||
|
-}
|
||||||
|
delete : { r | x : String } -> DeleteButtonConfig msg -> Html msg
|
||||||
|
delete assets config =
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "delete")
|
||||||
|
[ Css.display Css.inlineBlock
|
||||||
|
, Css.backgroundRepeat Css.noRepeat
|
||||||
|
, Css.backgroundColor Css.transparent
|
||||||
|
, Css.backgroundPosition Css.center
|
||||||
|
, Css.backgroundSize Css.contain
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.width (Css.px 15)
|
||||||
|
, Css.height (Css.px 15)
|
||||||
|
, Css.padding Css.zero
|
||||||
|
, Css.margin2 Css.zero (Css.px 6)
|
||||||
|
, Css.cursor Css.pointer
|
||||||
|
, Css.color Colors.azure
|
||||||
|
]
|
||||||
|
[ Events.onClick config.onClick
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
, -- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
Widget.label config.label
|
||||||
|
]
|
||||||
|
[ Icon.icon { alt = "Delete", icon = Icon.xSvg assets } ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- TOGGLE BUTTON
|
||||||
|
|
||||||
|
|
||||||
|
{-| Buttons can be toggled into a pressed state and back again.
|
||||||
|
-}
|
||||||
|
type alias ToggleButtonConfig msg =
|
||||||
|
{ label : String
|
||||||
|
, onSelect : msg
|
||||||
|
, onDeselect : msg
|
||||||
|
, pressed : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
toggleButton : ToggleButtonConfig msg -> Html msg
|
||||||
|
toggleButton config =
|
||||||
|
let
|
||||||
|
toggledStyles =
|
||||||
|
if config.pressed then
|
||||||
|
Css.batch
|
||||||
|
[ Css.color Colors.gray20
|
||||||
|
, Css.backgroundColor Colors.glacier
|
||||||
|
, Css.boxShadow5 Css.inset Css.zero (Css.px 3) Css.zero (ColorsExtra.withAlpha 0.2 Colors.gray20)
|
||||||
|
, Css.border3 (Css.px 1) Css.solid Colors.azure
|
||||||
|
, Css.fontWeight Css.bold
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
Css.batch
|
||||||
|
[]
|
||||||
|
in
|
||||||
|
Nri.Ui.styled Html.button
|
||||||
|
(styledName "toggleButton")
|
||||||
|
[ buttonStyles Medium WidthUnbounded SecondaryColors
|
||||||
|
, toggledStyles
|
||||||
|
]
|
||||||
|
[ Events.onClick
|
||||||
|
(if config.pressed then
|
||||||
|
config.onDeselect
|
||||||
|
|
||||||
|
else
|
||||||
|
config.onSelect
|
||||||
|
)
|
||||||
|
, Widget.pressed <| Just config.pressed
|
||||||
|
|
||||||
|
-- reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Labeling_buttons
|
||||||
|
, Role.button
|
||||||
|
|
||||||
|
-- Note: setting type: 'button' removes the default behavior of submit
|
||||||
|
-- equivalent to preventDefaultBehavior = false
|
||||||
|
-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-name
|
||||||
|
, Attributes.type_ "button"
|
||||||
|
]
|
||||||
|
[ viewLabel Nothing config.label ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Inputs can be a clickable thing used in a form
|
||||||
|
-}
|
||||||
|
type alias InputConfig =
|
||||||
|
{ content : Html Never
|
||||||
|
, name : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, value : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- LINKS THAT LOOK LIKE BUTTONS
|
||||||
|
|
||||||
|
|
||||||
|
{-| Links are clickable things with a url.
|
||||||
|
|
||||||
|
NOTE: Links do not support two-line labels.
|
||||||
|
|
||||||
|
-}
|
||||||
|
type alias LinkConfig =
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, url : String
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url
|
||||||
|
-}
|
||||||
|
link : LinkConfig -> Html msg
|
||||||
|
link =
|
||||||
|
linkBase "link" [ Attributes.target "_self" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Use this link for routing within a single page app.
|
||||||
|
|
||||||
|
This will make a normal <a> tag, but change the Events.onClick behavior to avoid reloading the page.
|
||||||
|
|
||||||
|
See <https://github.com/elm-lang/html/issues/110> for details on this implementation.
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkSpa :
|
||||||
|
(route -> String)
|
||||||
|
-> (route -> msg)
|
||||||
|
->
|
||||||
|
{ label : String
|
||||||
|
, icon : Maybe IconType
|
||||||
|
, size : ButtonSize
|
||||||
|
, style : ButtonStyle
|
||||||
|
, width : ButtonWidth
|
||||||
|
, route : route
|
||||||
|
}
|
||||||
|
-> Html msg
|
||||||
|
linkSpa toUrl toMsg config =
|
||||||
|
linkBase
|
||||||
|
"linkSpa"
|
||||||
|
[ EventExtras.onClickPreventDefaultForLinkWithHref (toMsg config.route)
|
||||||
|
]
|
||||||
|
{ label = config.label
|
||||||
|
, icon = config.icon
|
||||||
|
, size = config.size
|
||||||
|
, style = config.style
|
||||||
|
, width = config.width
|
||||||
|
, url = toUrl config.route
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url and have it open to an external site
|
||||||
|
-}
|
||||||
|
linkExternal : LinkConfig -> Html msg
|
||||||
|
linkExternal =
|
||||||
|
linkBase "linkExternal" [ Attributes.target "_blank" ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to
|
||||||
|
some url, and it's an HTTP request (Rails includes JS to make this use the given HTTP method)
|
||||||
|
-}
|
||||||
|
linkWithMethod : String -> LinkConfig -> Html msg
|
||||||
|
linkWithMethod method =
|
||||||
|
linkBase "linkWithMethod" [ Attributes.attribute "data-method" method ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url.
|
||||||
|
This should only take in messages that result in a Msg that triggers Analytics.trackAndRedirect. For buttons that trigger other effects on the page, please use Nri.Button.button instead
|
||||||
|
-}
|
||||||
|
linkWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkWithTracking"
|
||||||
|
[ Events.preventDefaultOn "click"
|
||||||
|
(Json.Decode.succeed ( onTrack, True ))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Wrap some text so it looks like a button, but actually is wrapped in an anchor to some url and have it open to an external site
|
||||||
|
|
||||||
|
This should only take in messages that result in tracking events. For buttons that trigger other effects on the page, please use Nri.Ui.Button.V2.button instead
|
||||||
|
|
||||||
|
-}
|
||||||
|
linkExternalWithTracking : msg -> LinkConfig -> Html msg
|
||||||
|
linkExternalWithTracking onTrack =
|
||||||
|
linkBase
|
||||||
|
"linkExternalWithTracking"
|
||||||
|
[ Attributes.target "_blank"
|
||||||
|
, EventExtras.onClickForLinkWithHref onTrack
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Helper function for building links with an arbitrary number of Attributes
|
||||||
|
-}
|
||||||
|
linkBase : String -> List (Attribute msg) -> LinkConfig -> Html msg
|
||||||
|
linkBase linkFunctionName extraAttrs config =
|
||||||
|
Nri.Ui.styled Styled.a
|
||||||
|
(styledName linkFunctionName)
|
||||||
|
[ buttonStyles config.size config.width (styleToColorPalette config.style) ]
|
||||||
|
(Attributes.href config.url
|
||||||
|
:: extraAttrs
|
||||||
|
)
|
||||||
|
[ viewLabel config.icon config.label ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HELPERS
|
||||||
|
|
||||||
|
|
||||||
|
type ColorPalette
|
||||||
|
= PrimaryColors
|
||||||
|
| SecondaryColors
|
||||||
|
| BorderlessColors
|
||||||
|
| DangerColors
|
||||||
|
| PremiumColors
|
||||||
|
| InactiveColors
|
||||||
|
| LoadingColors
|
||||||
|
| SuccessColors
|
||||||
|
| ErrorColors
|
||||||
|
|
||||||
|
|
||||||
|
styleToColorPalette : ButtonStyle -> ColorPalette
|
||||||
|
styleToColorPalette style =
|
||||||
|
case style of
|
||||||
|
Primary ->
|
||||||
|
PrimaryColors
|
||||||
|
|
||||||
|
Secondary ->
|
||||||
|
SecondaryColors
|
||||||
|
|
||||||
|
Borderless ->
|
||||||
|
BorderlessColors
|
||||||
|
|
||||||
|
Danger ->
|
||||||
|
DangerColors
|
||||||
|
|
||||||
|
Premium ->
|
||||||
|
PremiumColors
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyles : ButtonSize -> ButtonWidth -> ColorPalette -> Style
|
||||||
|
buttonStyles size width colorPalette =
|
||||||
|
Css.batch
|
||||||
|
[ buttonStyle
|
||||||
|
, colorStyle colorPalette
|
||||||
|
, sizeStyle size width
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewLabel : Maybe IconType -> String -> Html msg
|
||||||
|
viewLabel icn label =
|
||||||
|
Nri.Ui.styled Html.span
|
||||||
|
"button-label-span"
|
||||||
|
[ Css.overflow Css.hidden -- Keep scrollbars out of our button
|
||||||
|
, Css.overflowWrap Css.breakWord -- Ensure that words that exceed the button width break instead of disappearing
|
||||||
|
, Css.padding2 (Css.px 2) Css.zero -- Without a bit of bottom padding, text that extends below the baseline, like "g" gets cut off
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
(case icn of
|
||||||
|
Nothing ->
|
||||||
|
renderMarkdown label
|
||||||
|
|
||||||
|
Just iconType ->
|
||||||
|
Icon.decorativeIcon iconType :: renderMarkdown label
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
renderMarkdown : String -> List (Html msg)
|
||||||
|
renderMarkdown markdown =
|
||||||
|
case Markdown.Block.parse Nothing markdown of
|
||||||
|
-- It seems to be always first wrapped in a `Paragraph` and never directly a `PlainInline`
|
||||||
|
[ Markdown.Block.Paragraph _ inlines ] ->
|
||||||
|
List.map (Markdown.Inline.toHtml >> Styled.fromUnstyled) inlines
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
[ Html.text markdown ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- STYLES
|
||||||
|
|
||||||
|
|
||||||
|
buttonStyle : Style
|
||||||
|
buttonStyle =
|
||||||
|
Css.batch
|
||||||
|
[ Css.cursor Css.pointer
|
||||||
|
, -- Specifying the font can and should go away after bootstrap is removed from application.css
|
||||||
|
Nri.Ui.Fonts.V1.baseFont
|
||||||
|
, Css.textOverflow Css.ellipsis
|
||||||
|
, Css.overflow Css.hidden
|
||||||
|
, Css.textDecoration Css.none
|
||||||
|
, Css.backgroundImage Css.none
|
||||||
|
, Css.textShadow Css.none
|
||||||
|
, Css.property "transition" "background-color 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s, border-width 0s"
|
||||||
|
, Css.boxShadow Css.none
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.marginBottom Css.zero
|
||||||
|
, Css.hover [ Css.textDecoration Css.none ]
|
||||||
|
, Css.disabled [ Css.cursor Css.notAllowed ]
|
||||||
|
, Css.display Css.inlineFlex
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
colorStyle : ColorPalette -> Style
|
||||||
|
colorStyle colorPalette =
|
||||||
|
let
|
||||||
|
( config, additionalStyles ) =
|
||||||
|
case colorPalette of
|
||||||
|
PrimaryColors ->
|
||||||
|
( { background = Colors.azure
|
||||||
|
, hover = Colors.azureDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.azureDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SecondaryColors ->
|
||||||
|
( { background = Colors.white
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Just <| Colors.azure
|
||||||
|
, shadow = Colors.azure
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
BorderlessColors ->
|
||||||
|
( { background = Css.rgba 0 0 0 0
|
||||||
|
, hover = Css.rgba 0 0 0 0
|
||||||
|
, text = Colors.azure
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Css.rgba 0 0 0 0
|
||||||
|
}
|
||||||
|
, [ Css.hover
|
||||||
|
[ Css.textDecoration Css.underline
|
||||||
|
, Css.disabled [ Css.textDecoration Css.none ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
DangerColors ->
|
||||||
|
( { background = Colors.red
|
||||||
|
, hover = Colors.redDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.redDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
PremiumColors ->
|
||||||
|
( { background = Colors.yellow
|
||||||
|
, hover = Colors.ochre
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.ochre
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
InactiveColors ->
|
||||||
|
( { background = Colors.gray92
|
||||||
|
, hover = Colors.gray92
|
||||||
|
, text = Colors.gray45
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.gray92
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
LoadingColors ->
|
||||||
|
( { background = Colors.glacier
|
||||||
|
, hover = Colors.glacier
|
||||||
|
, text = Colors.navy
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.glacier
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
SuccessColors ->
|
||||||
|
( { background = Colors.greenDark
|
||||||
|
, hover = Colors.greenDark
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.greenDark
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
|
||||||
|
ErrorColors ->
|
||||||
|
( { background = Colors.purple
|
||||||
|
, hover = Colors.purple
|
||||||
|
, text = Colors.white
|
||||||
|
, border = Nothing
|
||||||
|
, shadow = Colors.purple
|
||||||
|
}
|
||||||
|
, []
|
||||||
|
)
|
||||||
|
in
|
||||||
|
Css.batch
|
||||||
|
[ Css.batch additionalStyles
|
||||||
|
, Css.color config.text
|
||||||
|
, Css.backgroundColor config.background
|
||||||
|
, Css.fontWeight (Css.int 700)
|
||||||
|
, Css.textAlign Css.center
|
||||||
|
, case config.border of
|
||||||
|
Nothing ->
|
||||||
|
Css.borderStyle Css.none
|
||||||
|
|
||||||
|
Just color ->
|
||||||
|
Css.batch
|
||||||
|
[ Css.borderColor color
|
||||||
|
, Css.borderStyle Css.solid
|
||||||
|
]
|
||||||
|
, Css.borderBottomStyle Css.solid
|
||||||
|
, Css.borderBottomColor config.shadow
|
||||||
|
, Css.fontStyle Css.normal
|
||||||
|
, Css.hover
|
||||||
|
[ Css.color config.text
|
||||||
|
, Css.backgroundColor config.hover
|
||||||
|
, Css.disabled [ Css.backgroundColor config.background ]
|
||||||
|
]
|
||||||
|
, Css.visited [ Css.color config.text ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
sizeStyle : ButtonSize -> ButtonWidth -> Style
|
||||||
|
sizeStyle size width =
|
||||||
|
let
|
||||||
|
config =
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
{ fontSize = 15
|
||||||
|
, height = 36
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 2
|
||||||
|
, minWidth = 75
|
||||||
|
}
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
{ fontSize = 17
|
||||||
|
, height = 45
|
||||||
|
, imageHeight = 15
|
||||||
|
, shadowHeight = 3
|
||||||
|
, minWidth = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
{ fontSize = 20
|
||||||
|
, height = 56
|
||||||
|
, imageHeight = 20
|
||||||
|
, shadowHeight = 4
|
||||||
|
, minWidth = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
sizingAttributes =
|
||||||
|
let
|
||||||
|
verticalPaddingPx =
|
||||||
|
2
|
||||||
|
in
|
||||||
|
[ Css.minHeight (Css.px config.height)
|
||||||
|
, Css.paddingTop (Css.px verticalPaddingPx)
|
||||||
|
, Css.paddingBottom (Css.px verticalPaddingPx)
|
||||||
|
]
|
||||||
|
|
||||||
|
widthAttributes =
|
||||||
|
case width of
|
||||||
|
WidthExact pxWidth ->
|
||||||
|
[ Css.maxWidth (Css.pct 100)
|
||||||
|
, Css.width (Css.px <| toFloat pxWidth)
|
||||||
|
, Css.paddingRight (Css.px 4)
|
||||||
|
, Css.paddingLeft (Css.px 4)
|
||||||
|
]
|
||||||
|
|
||||||
|
WidthUnbounded ->
|
||||||
|
[ Css.paddingLeft (Css.px 16)
|
||||||
|
, Css.paddingRight (Css.px 16)
|
||||||
|
, Css.minWidth (Css.px config.minWidth)
|
||||||
|
]
|
||||||
|
|
||||||
|
WidthFillContainer ->
|
||||||
|
[ Css.paddingLeft (Css.px 16)
|
||||||
|
, Css.paddingRight (Css.px 16)
|
||||||
|
, Css.minWidth (Css.px config.minWidth)
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
]
|
||||||
|
|
||||||
|
lineHeightPx =
|
||||||
|
case size of
|
||||||
|
Small ->
|
||||||
|
15
|
||||||
|
|
||||||
|
Medium ->
|
||||||
|
19
|
||||||
|
|
||||||
|
Large ->
|
||||||
|
22
|
||||||
|
in
|
||||||
|
Css.batch
|
||||||
|
[ Css.fontSize (Css.px config.fontSize)
|
||||||
|
, Css.borderRadius (Css.px 8)
|
||||||
|
, Css.lineHeight (Css.px lineHeightPx)
|
||||||
|
, Css.boxSizing Css.borderBox
|
||||||
|
, Css.borderWidth (Css.px 1)
|
||||||
|
, Css.borderBottomWidth (Css.px config.shadowHeight)
|
||||||
|
, Css.batch sizingAttributes
|
||||||
|
, Css.batch widthAttributes
|
||||||
|
, Css.Global.descendants
|
||||||
|
[ Css.Global.img
|
||||||
|
[ Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.height (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.width (Css.px config.imageHeight) |> Css.important
|
||||||
|
, Css.marginRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
, Css.Global.svg
|
||||||
|
[ Css.important <| Css.height (Css.px config.imageHeight)
|
||||||
|
, Css.important <| Css.width Css.auto
|
||||||
|
, Css.maxWidth (Css.px (config.imageHeight * 1.25))
|
||||||
|
, Css.paddingRight (Css.px <| config.imageHeight / 6)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.bottom (Css.px 2)
|
||||||
|
, Css.verticalAlign Css.middle
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
styledName : String -> String
|
||||||
|
styledName suffix =
|
||||||
|
"Nri-Ui-Button-V7-" ++ suffix
|
199
src/Nri/Ui/Heading/V1.elm
Normal file
199
src/Nri/Ui/Heading/V1.elm
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
module Nri.Ui.Heading.V1 exposing
|
||||||
|
( Heading, heading
|
||||||
|
, withVisualLevel, VisualLevel(..)
|
||||||
|
, withDocumentLevel, DocumentLevel(..)
|
||||||
|
, view
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| Headings with customization options for accessibility.
|
||||||
|
|
||||||
|
|
||||||
|
## Understanding spacing
|
||||||
|
|
||||||
|
- All text styles have a specific line-height. This is set so that when text
|
||||||
|
in the given style is long enough to wrap, the spacing between wrapped lines
|
||||||
|
looks good.
|
||||||
|
- No heading styles have padding.
|
||||||
|
- **Heading styles** do not have margin. It is up to the caller to add
|
||||||
|
appropriate margin to the layout.
|
||||||
|
|
||||||
|
@docs Heading, heading
|
||||||
|
|
||||||
|
@docs withVisualLevel, VisualLevel
|
||||||
|
|
||||||
|
@docs withDocumentLevel, DocumentLevel
|
||||||
|
|
||||||
|
@docs view
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Css exposing (..)
|
||||||
|
import Html.Styled exposing (..)
|
||||||
|
import Html.Styled.Attributes exposing (css)
|
||||||
|
import Nri.Ui.Colors.V1 exposing (..)
|
||||||
|
import Nri.Ui.Fonts.V1 as Fonts
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type Heading msg
|
||||||
|
= Heading (List (Html msg)) VisualLevel DocumentLevel
|
||||||
|
|
||||||
|
|
||||||
|
{-| start a heading. Render it with `view`.
|
||||||
|
-}
|
||||||
|
heading : List (Html msg) -> Heading msg
|
||||||
|
heading content =
|
||||||
|
Heading content Top H1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- VISUAL LEVEL
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type VisualLevel
|
||||||
|
= Top
|
||||||
|
| Tagline
|
||||||
|
| Subhead
|
||||||
|
| Small
|
||||||
|
|
||||||
|
|
||||||
|
{-| Customize how the heading looks. The visual hierarchy should go Top ->
|
||||||
|
Tagline -> Subhead -> Small.
|
||||||
|
|
||||||
|
heading [ text "Hello, World!" ]
|
||||||
|
|> withVisualLevel Top
|
||||||
|
|> view
|
||||||
|
|
||||||
|
-}
|
||||||
|
withVisualLevel : VisualLevel -> Heading msg -> Heading msg
|
||||||
|
withVisualLevel visualLevel (Heading content _ documentLevel) =
|
||||||
|
Heading content visualLevel documentLevel
|
||||||
|
|
||||||
|
|
||||||
|
getStyles : VisualLevel -> Style
|
||||||
|
getStyles visualLevel =
|
||||||
|
case visualLevel of
|
||||||
|
Top ->
|
||||||
|
headingStyles
|
||||||
|
{ font = Fonts.baseFont
|
||||||
|
, color = navy
|
||||||
|
, size = 30
|
||||||
|
, lineHeight = 38
|
||||||
|
, weight = 700
|
||||||
|
}
|
||||||
|
|
||||||
|
Tagline ->
|
||||||
|
headingStyles
|
||||||
|
{ font = Fonts.baseFont
|
||||||
|
, color = gray45
|
||||||
|
, size = 20
|
||||||
|
, lineHeight = 30
|
||||||
|
, weight = 400
|
||||||
|
}
|
||||||
|
|
||||||
|
Subhead ->
|
||||||
|
headingStyles
|
||||||
|
{ font = Fonts.baseFont
|
||||||
|
, color = navy
|
||||||
|
, size = 20
|
||||||
|
, lineHeight = 26
|
||||||
|
, weight = 700
|
||||||
|
}
|
||||||
|
|
||||||
|
Small ->
|
||||||
|
Css.batch
|
||||||
|
[ headingStyles
|
||||||
|
{ font = Fonts.baseFont
|
||||||
|
, color = gray20
|
||||||
|
, size = 16
|
||||||
|
, lineHeight = 21
|
||||||
|
, weight = 700
|
||||||
|
}
|
||||||
|
, letterSpacing (px -0.13)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
headingStyles :
|
||||||
|
{ color : Color
|
||||||
|
, font : Style
|
||||||
|
, lineHeight : Float
|
||||||
|
, size : Float
|
||||||
|
, weight : Int
|
||||||
|
}
|
||||||
|
-> Style
|
||||||
|
headingStyles config =
|
||||||
|
Css.batch
|
||||||
|
[ config.font
|
||||||
|
, fontSize (px config.size)
|
||||||
|
, color config.color
|
||||||
|
, lineHeight (px config.lineHeight)
|
||||||
|
, fontWeight (int config.weight)
|
||||||
|
, padding zero
|
||||||
|
, textAlign left
|
||||||
|
, margin zero
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- DOCUMENT LEVEL
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type DocumentLevel
|
||||||
|
= H1
|
||||||
|
| H2
|
||||||
|
| H3
|
||||||
|
| H4
|
||||||
|
| H5
|
||||||
|
| H6
|
||||||
|
|
||||||
|
|
||||||
|
{-| Customize the document level of the heading. For accessibility reasons, you
|
||||||
|
should have exactly one H1, and only increase the level by one. You can use a
|
||||||
|
tool like [axe](https://www.deque.com/axe/) to check this.
|
||||||
|
|
||||||
|
heading [ text "Hello, World!" ]
|
||||||
|
|> withDocumentLevel H1
|
||||||
|
|> view
|
||||||
|
|
||||||
|
-}
|
||||||
|
withDocumentLevel : DocumentLevel -> Heading msg -> Heading msg
|
||||||
|
withDocumentLevel documentLevel (Heading content visualLevel _) =
|
||||||
|
Heading content visualLevel documentLevel
|
||||||
|
|
||||||
|
|
||||||
|
getTag : DocumentLevel -> (List (Attribute msg) -> List (Html msg) -> Html msg)
|
||||||
|
getTag documentLevel =
|
||||||
|
case documentLevel of
|
||||||
|
H1 ->
|
||||||
|
h1
|
||||||
|
|
||||||
|
H2 ->
|
||||||
|
h2
|
||||||
|
|
||||||
|
H3 ->
|
||||||
|
h3
|
||||||
|
|
||||||
|
H4 ->
|
||||||
|
h4
|
||||||
|
|
||||||
|
H5 ->
|
||||||
|
h5
|
||||||
|
|
||||||
|
H6 ->
|
||||||
|
h6
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
|
{-| render a Heading to Html. See the other docs in this module for
|
||||||
|
customizations.
|
||||||
|
-}
|
||||||
|
view : Heading msg -> Html msg
|
||||||
|
view (Heading content visualLevel documentLevel) =
|
||||||
|
getTag documentLevel
|
||||||
|
[ css [ getStyles visualLevel ] ]
|
||||||
|
content
|
213
src/Nri/Ui/Modal/V2.elm
Normal file
213
src/Nri/Ui/Modal/V2.elm
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
module Nri.Ui.Modal.V2 exposing
|
||||||
|
( Model
|
||||||
|
, info
|
||||||
|
, warning
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| Changes from V1:
|
||||||
|
|
||||||
|
- Use Styled Html
|
||||||
|
|
||||||
|
@docs Model
|
||||||
|
@docs info
|
||||||
|
@docs warning
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (..)
|
||||||
|
import Accessibility.Styled.Role as Role
|
||||||
|
import Accessibility.Styled.Widget as Widget
|
||||||
|
import Css
|
||||||
|
import Css.Global exposing (Snippet, body, children, descendants, everything, selector)
|
||||||
|
import Html.Styled
|
||||||
|
import Html.Styled.Events exposing (onClick)
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.Colors.Extra
|
||||||
|
import Nri.Ui.Colors.V1
|
||||||
|
import Nri.Ui.Fonts.V1 as Fonts
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
- `onDismiss`: If `Nothing`, the modal will not be dismissable
|
||||||
|
- `visibleTitle`: If `False`, the title will still be used for screen readers
|
||||||
|
- `content`: This will be placed in a `width:100%` div in the main area of the modal
|
||||||
|
- `footerContent`: The optional items here will be stacked below the main content area and center-aligned.
|
||||||
|
Commonly you will either give a list of Nri.Ui.Buttons,
|
||||||
|
or an empty list.
|
||||||
|
|
||||||
|
-}
|
||||||
|
type alias Model msg =
|
||||||
|
{ title : String
|
||||||
|
, visibleTitle : Bool
|
||||||
|
, content : Html msg
|
||||||
|
, footerContent : List (Html msg)
|
||||||
|
, onDismiss : Maybe msg
|
||||||
|
, width : Maybe Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ModalType
|
||||||
|
= Info
|
||||||
|
| Warning
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
info : Model msg -> Html msg
|
||||||
|
info =
|
||||||
|
view Info
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
warning : Model msg -> Html msg
|
||||||
|
warning =
|
||||||
|
view Warning
|
||||||
|
|
||||||
|
|
||||||
|
view : ModalType -> Model msg -> Html msg
|
||||||
|
view modalType { title, visibleTitle, content, onDismiss, footerContent, width } =
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-backdrop-container"
|
||||||
|
((case modalType of
|
||||||
|
Info ->
|
||||||
|
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.navy)
|
||||||
|
|
||||||
|
Warning ->
|
||||||
|
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.gray20)
|
||||||
|
)
|
||||||
|
:: [ Css.height (Css.vh 100)
|
||||||
|
, Css.left Css.zero
|
||||||
|
, Css.overflow Css.hidden
|
||||||
|
, Css.position Css.fixed
|
||||||
|
, Css.top Css.zero
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
, Css.zIndex (Css.int 200)
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[ Role.dialog
|
||||||
|
, Widget.label title
|
||||||
|
, Widget.modal True
|
||||||
|
]
|
||||||
|
[ Nri.Ui.styled Html.Styled.div
|
||||||
|
"modal-click-catcher"
|
||||||
|
[ Css.bottom Css.zero
|
||||||
|
, Css.left Css.zero
|
||||||
|
, Css.position Css.absolute
|
||||||
|
, Css.right Css.zero
|
||||||
|
, Css.top Css.zero
|
||||||
|
]
|
||||||
|
(case onDismiss of
|
||||||
|
Nothing ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
Just msg ->
|
||||||
|
[ onClick msg ]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
, Nri.Ui.styled div
|
||||||
|
"modal-container"
|
||||||
|
[ Css.width (Css.px 600)
|
||||||
|
, Css.maxHeight <| Css.calc (Css.vh 100) Css.minus (Css.px 100)
|
||||||
|
, Css.padding4 (Css.px 35) Css.zero (Css.px 25) Css.zero
|
||||||
|
, Css.margin2 (Css.px 75) Css.auto
|
||||||
|
, Css.backgroundColor Nri.Ui.Colors.V1.white
|
||||||
|
, Css.borderRadius (Css.px 20)
|
||||||
|
, Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||||
|
, Css.position Css.relative -- required for closeButtonContainer
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.flexDirection Css.column
|
||||||
|
, Css.flexWrap Css.noWrap
|
||||||
|
, Fonts.baseFont
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ -- This global <style> node sets overflow to hidden on the body element,
|
||||||
|
-- thereby preventing the page from scrolling behind the backdrop when the modal is
|
||||||
|
-- open (and this node is present on the page).
|
||||||
|
Css.Global.global
|
||||||
|
[ Css.Global.body
|
||||||
|
[ Css.overflow Css.hidden ]
|
||||||
|
]
|
||||||
|
, if visibleTitle then
|
||||||
|
viewHeader modalType title
|
||||||
|
|
||||||
|
else
|
||||||
|
text ""
|
||||||
|
, viewContent modalType content
|
||||||
|
, viewFooter footerContent
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewHeader : ModalType -> String -> Html msg
|
||||||
|
viewHeader modalType title =
|
||||||
|
Nri.Ui.styled Html.h3
|
||||||
|
"modal-header"
|
||||||
|
((case modalType of
|
||||||
|
Info ->
|
||||||
|
Css.color Nri.Ui.Colors.V1.navy
|
||||||
|
|
||||||
|
Warning ->
|
||||||
|
Css.color Nri.Ui.Colors.V1.red
|
||||||
|
)
|
||||||
|
:: [ Css.fontWeight (Css.int 700)
|
||||||
|
, Css.lineHeight (Css.px 27)
|
||||||
|
, Css.margin2 Css.zero (Css.px 65)
|
||||||
|
, Css.fontSize (Css.px 20)
|
||||||
|
, Fonts.baseFont
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
[ Html.text title
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewContent : ModalType -> Html msg -> Html msg
|
||||||
|
viewContent modalType content =
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-content"
|
||||||
|
[ Css.overflowY Css.scroll
|
||||||
|
, Css.padding2 (Css.px 30) (Css.px 45)
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
, Css.minHeight (Css.px 150)
|
||||||
|
, Css.boxSizing Css.borderBox
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ content ]
|
||||||
|
|
||||||
|
|
||||||
|
viewFooter : List (Html msg) -> Html msg
|
||||||
|
viewFooter footerContent =
|
||||||
|
case footerContent of
|
||||||
|
[] ->
|
||||||
|
Html.text ""
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-footer"
|
||||||
|
[ Css.alignItems Css.center
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.flexDirection Css.column
|
||||||
|
, Css.flexGrow (Css.int 2)
|
||||||
|
, Css.flexWrap Css.noWrap
|
||||||
|
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
(List.map
|
||||||
|
(\x ->
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-footer-item"
|
||||||
|
[ Css.margin4 (Css.px 10) Css.zero Css.zero Css.zero
|
||||||
|
, Css.firstChild
|
||||||
|
[ Css.margin Css.zero
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ x ]
|
||||||
|
)
|
||||||
|
footerContent
|
||||||
|
)
|
246
src/Nri/Ui/Modal/V3.elm
Normal file
246
src/Nri/Ui/Modal/V3.elm
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
module Nri.Ui.Modal.V3 exposing
|
||||||
|
( Model
|
||||||
|
, info
|
||||||
|
, warning
|
||||||
|
)
|
||||||
|
|
||||||
|
{-| Changes from V2:
|
||||||
|
|
||||||
|
- Add assets for close button
|
||||||
|
|
||||||
|
@docs Model
|
||||||
|
@docs info
|
||||||
|
@docs warning
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled as Html exposing (..)
|
||||||
|
import Accessibility.Styled.Role as Role
|
||||||
|
import Accessibility.Styled.Widget as Widget
|
||||||
|
import Css
|
||||||
|
import Css.Global exposing (Snippet, body, children, descendants, everything, selector)
|
||||||
|
import Html.Styled
|
||||||
|
import Html.Styled.Events exposing (onClick)
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.AssetPath exposing (Asset(..))
|
||||||
|
import Nri.Ui.Colors.Extra
|
||||||
|
import Nri.Ui.Colors.V1
|
||||||
|
import Nri.Ui.Fonts.V1 as Fonts
|
||||||
|
import Nri.Ui.Icon.V3 as Icon
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
- `onDismiss`: If `Nothing`, the modal will not be dismissable
|
||||||
|
- `visibleTitle`: If `False`, the title will still be used for screen readers
|
||||||
|
- `content`: This will be placed in a `width:100%` div in the main area of the modal
|
||||||
|
- `footerContent`: The optional items here will be stacked below the main content area and center-aligned.
|
||||||
|
Commonly you will either give a list of Nri.Ui.Buttons,
|
||||||
|
or an empty list.
|
||||||
|
|
||||||
|
-}
|
||||||
|
type alias Model msg =
|
||||||
|
{ title : String
|
||||||
|
, visibleTitle : Bool
|
||||||
|
, content : Html msg
|
||||||
|
, footerContent : List (Html msg)
|
||||||
|
, onDismiss : Maybe msg
|
||||||
|
, width : Maybe Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Assets r =
|
||||||
|
{ r | icons_xBlue_svg : Asset }
|
||||||
|
|
||||||
|
|
||||||
|
type ModalType
|
||||||
|
= Info
|
||||||
|
| Warning
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
info : Assets r -> Model msg -> Html msg
|
||||||
|
info assets =
|
||||||
|
view assets Info
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
warning : Assets r -> Model msg -> Html msg
|
||||||
|
warning assets =
|
||||||
|
view assets Warning
|
||||||
|
|
||||||
|
|
||||||
|
view : Assets r -> ModalType -> Model msg -> Html msg
|
||||||
|
view assets modalType { title, visibleTitle, content, onDismiss, footerContent, width } =
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-backdrop-container"
|
||||||
|
((case modalType of
|
||||||
|
Info ->
|
||||||
|
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.navy)
|
||||||
|
|
||||||
|
Warning ->
|
||||||
|
Css.backgroundColor (Nri.Ui.Colors.Extra.withAlpha 0.9 Nri.Ui.Colors.V1.gray20)
|
||||||
|
)
|
||||||
|
:: [ Css.height (Css.vh 100)
|
||||||
|
, Css.left Css.zero
|
||||||
|
, Css.overflow Css.hidden
|
||||||
|
, Css.position Css.fixed
|
||||||
|
, Css.top Css.zero
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
, Css.zIndex (Css.int 200)
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.justifyContent Css.center
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[ Role.dialog
|
||||||
|
, Widget.label title
|
||||||
|
, Widget.modal True
|
||||||
|
]
|
||||||
|
[ Nri.Ui.styled Html.Styled.div
|
||||||
|
"modal-click-catcher"
|
||||||
|
[ Css.bottom Css.zero
|
||||||
|
, Css.left Css.zero
|
||||||
|
, Css.position Css.absolute
|
||||||
|
, Css.right Css.zero
|
||||||
|
, Css.top Css.zero
|
||||||
|
]
|
||||||
|
(case onDismiss of
|
||||||
|
Nothing ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
Just msg ->
|
||||||
|
[ onClick msg ]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
, Nri.Ui.styled div
|
||||||
|
"modal-container"
|
||||||
|
[ Css.width (Css.px 600)
|
||||||
|
, Css.maxHeight <| Css.calc (Css.vh 100) Css.minus (Css.px 100)
|
||||||
|
, Css.padding4 (Css.px 40) Css.zero (Css.px 40) Css.zero
|
||||||
|
, Css.margin2 (Css.px 75) Css.auto
|
||||||
|
, Css.backgroundColor Nri.Ui.Colors.V1.white
|
||||||
|
, Css.borderRadius (Css.px 20)
|
||||||
|
, Css.property "box-shadow" "0 1px 10px 0 rgba(0, 0, 0, 0.35)"
|
||||||
|
, Css.position Css.relative -- required for closeButtonContainer
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.alignItems Css.center
|
||||||
|
, Css.flexDirection Css.column
|
||||||
|
, Css.flexWrap Css.noWrap
|
||||||
|
, Fonts.baseFont
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ -- This global <style> node sets overflow to hidden on the body element,
|
||||||
|
-- thereby preventing the page from scrolling behind the backdrop when the modal is
|
||||||
|
-- open (and this node is present on the page).
|
||||||
|
Css.Global.global
|
||||||
|
[ Css.Global.body
|
||||||
|
[ Css.overflow Css.hidden ]
|
||||||
|
]
|
||||||
|
, case onDismiss of
|
||||||
|
Just msg ->
|
||||||
|
closeButton assets msg
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
text ""
|
||||||
|
, if visibleTitle then
|
||||||
|
viewHeader modalType title
|
||||||
|
|
||||||
|
else
|
||||||
|
text ""
|
||||||
|
, viewContent modalType content
|
||||||
|
, viewFooter footerContent
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
closeButton : Assets r -> msg -> Html msg
|
||||||
|
closeButton assets msg =
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"close-button-container"
|
||||||
|
[ Css.position Css.absolute
|
||||||
|
, Css.top Css.zero
|
||||||
|
, Css.right Css.zero
|
||||||
|
, Css.padding (Css.px 25)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ Icon.button
|
||||||
|
{ alt = "Close"
|
||||||
|
, msg = msg
|
||||||
|
, icon = Icon.close assets
|
||||||
|
, disabled = False
|
||||||
|
, size = Icon.Medium
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewHeader : ModalType -> String -> Html msg
|
||||||
|
viewHeader modalType title =
|
||||||
|
Nri.Ui.styled Html.h3
|
||||||
|
"modal-header"
|
||||||
|
((case modalType of
|
||||||
|
Info ->
|
||||||
|
Css.color Nri.Ui.Colors.V1.navy
|
||||||
|
|
||||||
|
Warning ->
|
||||||
|
Css.color Nri.Ui.Colors.V1.red
|
||||||
|
)
|
||||||
|
:: [ Css.fontWeight (Css.int 700)
|
||||||
|
, Css.lineHeight (Css.px 27)
|
||||||
|
, Css.margin2 Css.zero (Css.px 49)
|
||||||
|
, Css.fontSize (Css.px 20)
|
||||||
|
, Fonts.baseFont
|
||||||
|
, Css.textAlign Css.center
|
||||||
|
]
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
[ Html.text title
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewContent : ModalType -> Html msg -> Html msg
|
||||||
|
viewContent modalType content =
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-content"
|
||||||
|
[ Css.overflowY Css.auto
|
||||||
|
, Css.padding2 (Css.px 30) (Css.px 40)
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
, Css.minHeight (Css.px 150)
|
||||||
|
, Css.boxSizing Css.borderBox
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ content ]
|
||||||
|
|
||||||
|
|
||||||
|
viewFooter : List (Html msg) -> Html msg
|
||||||
|
viewFooter footerContent =
|
||||||
|
case footerContent of
|
||||||
|
[] ->
|
||||||
|
Html.text ""
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-footer"
|
||||||
|
[ Css.alignItems Css.center
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.flexDirection Css.column
|
||||||
|
, Css.flexGrow (Css.int 2)
|
||||||
|
, Css.flexWrap Css.noWrap
|
||||||
|
, Css.margin4 (Css.px 20) Css.zero Css.zero Css.zero
|
||||||
|
, Css.width (Css.pct 100)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
(List.map
|
||||||
|
(\x ->
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"modal-footer-item"
|
||||||
|
[ Css.margin4 (Css.px 10) Css.zero Css.zero Css.zero
|
||||||
|
, Css.firstChild
|
||||||
|
[ Css.margin Css.zero
|
||||||
|
]
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ x ]
|
||||||
|
)
|
||||||
|
footerContent
|
||||||
|
)
|
201
src/Nri/Ui/Page/V2.elm
Normal file
201
src/Nri/Ui/Page/V2.elm
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
module Nri.Ui.Page.V2 exposing (DefaultPage, broken, blocked, notFound, noPermission)
|
||||||
|
|
||||||
|
{-| A styled NRI issue page!
|
||||||
|
|
||||||
|
@docs DefaultPage, broken, blocked, notFound, noPermission
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Css exposing (..)
|
||||||
|
import Html.Styled as Html exposing (Html)
|
||||||
|
import Html.Styled.Attributes as Attributes
|
||||||
|
import Nri.Ui.Button.V5 as Button
|
||||||
|
import Nri.Ui.Text.V2 as Text
|
||||||
|
|
||||||
|
|
||||||
|
{-| The default page information is for the button
|
||||||
|
which will direct the user back to the main page of
|
||||||
|
the SPA. Specify it's name and the message which will
|
||||||
|
navigate to the page.
|
||||||
|
-}
|
||||||
|
type alias DefaultPage msg =
|
||||||
|
{ link : msg
|
||||||
|
, name : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| For the not found page.
|
||||||
|
-}
|
||||||
|
notFound : DefaultPage msg -> Html msg
|
||||||
|
notFound defaultPage =
|
||||||
|
view
|
||||||
|
{ emoji = "\u{1F914}"
|
||||||
|
, title = "We couldn’t find that!"
|
||||||
|
, subtitle = "Feel free to browse around, or check out our help center."
|
||||||
|
, defaultPage = Just defaultPage
|
||||||
|
, details = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| For HTTP errors and other broken states.
|
||||||
|
-}
|
||||||
|
broken : DefaultPage msg -> Html msg
|
||||||
|
broken defaultPage =
|
||||||
|
view
|
||||||
|
{ emoji = "😵"
|
||||||
|
, title = "There was a problem!"
|
||||||
|
, subtitle = "You can try again, or check out our help center."
|
||||||
|
, defaultPage = Just defaultPage
|
||||||
|
, details = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| For HTTP errors and other broken states, where link goes to "/".
|
||||||
|
-}
|
||||||
|
blocked : String -> Html msg
|
||||||
|
blocked details =
|
||||||
|
view
|
||||||
|
{ emoji = "😵"
|
||||||
|
, title = "There was a problem!"
|
||||||
|
, subtitle = "You can try again, or check out our help center."
|
||||||
|
, defaultPage = Nothing
|
||||||
|
, details = Just details
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| For pages the user does not have access to.
|
||||||
|
-}
|
||||||
|
noPermission : DefaultPage msg -> Html msg
|
||||||
|
noPermission defaultPage =
|
||||||
|
view
|
||||||
|
{ emoji = "\u{1F910}"
|
||||||
|
, title = "You do not have access to this page!"
|
||||||
|
, subtitle = "Talk to a site administrator if you believe you should have access to this page."
|
||||||
|
, defaultPage = Just defaultPage
|
||||||
|
, details = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- INTERNAL
|
||||||
|
|
||||||
|
|
||||||
|
type alias Config msg =
|
||||||
|
{ emoji : String
|
||||||
|
, title : String
|
||||||
|
, subtitle : String
|
||||||
|
, defaultPage : Maybe (DefaultPage msg)
|
||||||
|
, details : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
view : Config msg -> Html msg
|
||||||
|
view config =
|
||||||
|
viewContainer
|
||||||
|
[ viewEmoji [ Html.text config.emoji ]
|
||||||
|
, Text.heading [ Html.text config.title ]
|
||||||
|
, Text.tagline [ Html.text config.subtitle ]
|
||||||
|
, viewButton
|
||||||
|
[ viewExit config ]
|
||||||
|
, viewButton
|
||||||
|
[ Button.linkExternal
|
||||||
|
{ label = "Get help!"
|
||||||
|
, icon = Nothing
|
||||||
|
, url = "https://noredink.zendesk.com/hc/en-us"
|
||||||
|
, size = Button.Large
|
||||||
|
, style = Button.Secondary
|
||||||
|
, width = Button.WidthExact 260
|
||||||
|
}
|
||||||
|
]
|
||||||
|
, case config.details of
|
||||||
|
Just details ->
|
||||||
|
viewButton [ viewDetails details ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Html.text ""
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewExit : Config msg -> Html msg
|
||||||
|
viewExit config =
|
||||||
|
case config.defaultPage of
|
||||||
|
Just defaultPage ->
|
||||||
|
Button.button
|
||||||
|
{ onClick = defaultPage.link
|
||||||
|
, size = Button.Large
|
||||||
|
, style = Button.Primary
|
||||||
|
, width = Button.WidthExact 260
|
||||||
|
}
|
||||||
|
{ label = "Return to " ++ defaultPage.name
|
||||||
|
, state = Button.Enabled
|
||||||
|
, icon = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Button.link
|
||||||
|
{ label = "Return to dashboard"
|
||||||
|
, icon = Nothing
|
||||||
|
, url = "/"
|
||||||
|
, size = Button.Large
|
||||||
|
, style = Button.Primary
|
||||||
|
, width = Button.WidthExact 260
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
viewDetails : String -> Html msg
|
||||||
|
viewDetails detailsForEngineers =
|
||||||
|
Html.div []
|
||||||
|
[ Html.styled Html.details
|
||||||
|
[ margin (px 10)
|
||||||
|
, maxWidth (px 700)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ Html.styled Html.summary
|
||||||
|
[ color (hex "8F8F8F") ]
|
||||||
|
[]
|
||||||
|
[ Html.text "Details for NoRedInk engineers" ]
|
||||||
|
, Html.styled Html.code
|
||||||
|
[ display block
|
||||||
|
, whiteSpace normal
|
||||||
|
, overflowWrap breakWord
|
||||||
|
, textAlign left
|
||||||
|
, marginTop (px 10)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ Html.text detailsForEngineers ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewContainer : List (Html msg) -> Html msg
|
||||||
|
viewContainer =
|
||||||
|
Html.styled Html.div
|
||||||
|
[ marginTop (px 80)
|
||||||
|
, displayFlex
|
||||||
|
, flexDirection column
|
||||||
|
, alignItems center
|
||||||
|
]
|
||||||
|
[ Attributes.attribute "data-page-container" "" ]
|
||||||
|
|
||||||
|
|
||||||
|
viewButton : List (Html msg) -> Html msg
|
||||||
|
viewButton children =
|
||||||
|
Html.styled Html.div
|
||||||
|
[ marginTop (px 15)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ Html.styled Html.div
|
||||||
|
[ textAlign center ]
|
||||||
|
[]
|
||||||
|
children
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewEmoji : List (Html msg) -> Html msg
|
||||||
|
viewEmoji =
|
||||||
|
Html.styled Html.div
|
||||||
|
[ fontSize (px 75)
|
||||||
|
, height (px 98)
|
||||||
|
, lineHeight (px 98)
|
||||||
|
]
|
||||||
|
[]
|
150
src/Nri/Ui/SegmentedControl/V6.elm
Normal file
150
src/Nri/Ui/SegmentedControl/V6.elm
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
module Nri.Ui.SegmentedControl.V6 exposing (Config, Icon, Option, Width(..), view)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
@docs Config, Icon, Option, Width, view
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled exposing (..)
|
||||||
|
import Accessibility.Styled.Role as Role
|
||||||
|
import Css exposing (..)
|
||||||
|
import Html.Styled as Html exposing (Html)
|
||||||
|
import Html.Styled.Attributes as Attr exposing (css)
|
||||||
|
import Html.Styled.Events as Events
|
||||||
|
import Nri.Ui
|
||||||
|
import Nri.Ui.Colors.Extra exposing (withAlpha)
|
||||||
|
import Nri.Ui.Colors.V1 as Colors
|
||||||
|
import Nri.Ui.Fonts.V1 as Fonts
|
||||||
|
import Nri.Ui.Icon.V3 as Icon
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type alias Config a msg =
|
||||||
|
{ onClick : a -> msg
|
||||||
|
, options : List (Option a)
|
||||||
|
, selected : a
|
||||||
|
, width : Width
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type alias Option a =
|
||||||
|
{ value : a
|
||||||
|
, icon : Maybe Icon
|
||||||
|
, label : String
|
||||||
|
, id : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type Width
|
||||||
|
= FitContent
|
||||||
|
| FillContainer
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type alias Icon =
|
||||||
|
{ alt : String
|
||||||
|
, icon : Icon.IconType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
view : Config a msg -> Html.Html msg
|
||||||
|
view config =
|
||||||
|
tabList <|
|
||||||
|
List.map (viewTab config) config.options
|
||||||
|
|
||||||
|
|
||||||
|
tabList : List (Html.Html msg) -> Html.Html msg
|
||||||
|
tabList =
|
||||||
|
Nri.Ui.styled div
|
||||||
|
"Nri-Ui-SegmentedControl-tabList"
|
||||||
|
[ displayFlex, cursor pointer ]
|
||||||
|
[ Role.tabList ]
|
||||||
|
|
||||||
|
|
||||||
|
viewTab : Config a msg -> Option a -> Html.Html msg
|
||||||
|
viewTab config option =
|
||||||
|
Html.div
|
||||||
|
[ Attr.id option.id
|
||||||
|
, Role.tab
|
||||||
|
, Events.onClick (config.onClick option.value)
|
||||||
|
, css sharedTabStyles
|
||||||
|
, css <|
|
||||||
|
if option.value == config.selected then
|
||||||
|
focusedTabStyles
|
||||||
|
|
||||||
|
else
|
||||||
|
unFocusedTabStyles
|
||||||
|
, css <|
|
||||||
|
case config.width of
|
||||||
|
FitContent ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
FillContainer ->
|
||||||
|
expandingTabStyles
|
||||||
|
]
|
||||||
|
[ case option.icon of
|
||||||
|
Nothing ->
|
||||||
|
Html.text ""
|
||||||
|
|
||||||
|
Just icon ->
|
||||||
|
viewIcon icon
|
||||||
|
, Html.text option.label
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewIcon : Icon -> Html.Html msg
|
||||||
|
viewIcon icon =
|
||||||
|
Html.span
|
||||||
|
[ css [ marginRight (px 10) ] ]
|
||||||
|
[ Icon.icon icon ]
|
||||||
|
|
||||||
|
|
||||||
|
sharedTabStyles : List Style
|
||||||
|
sharedTabStyles =
|
||||||
|
[ padding2 (px 6) (px 20)
|
||||||
|
, height (px 45)
|
||||||
|
, Fonts.baseFont
|
||||||
|
, fontSize (px 15)
|
||||||
|
, color Colors.azure
|
||||||
|
, fontWeight bold
|
||||||
|
, lineHeight (px 30)
|
||||||
|
, firstOfType
|
||||||
|
[ borderTopLeftRadius (px 8)
|
||||||
|
, borderBottomLeftRadius (px 8)
|
||||||
|
, borderLeft3 (px 1) solid Colors.azure
|
||||||
|
]
|
||||||
|
, lastOfType
|
||||||
|
[ borderTopRightRadius (px 8)
|
||||||
|
, borderBottomRightRadius (px 8)
|
||||||
|
]
|
||||||
|
, border3 (px 1) solid Colors.azure
|
||||||
|
, borderLeft (px 0)
|
||||||
|
, boxSizing borderBox
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
focusedTabStyles : List Style
|
||||||
|
focusedTabStyles =
|
||||||
|
[ backgroundColor Colors.glacier
|
||||||
|
, boxShadow5 inset zero (px 3) zero (withAlpha 0.2 Colors.gray20)
|
||||||
|
, color Colors.gray20
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
unFocusedTabStyles : List Style
|
||||||
|
unFocusedTabStyles =
|
||||||
|
[ backgroundColor Colors.white
|
||||||
|
, boxShadow5 inset zero (px -2) zero Colors.azure
|
||||||
|
, color Colors.azure
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
expandingTabStyles : List Style
|
||||||
|
expandingTabStyles =
|
||||||
|
[ flexGrow (int 1)
|
||||||
|
, textAlign center
|
||||||
|
]
|
368
src/Nri/Ui/Tabs/V3.elm
Normal file
368
src/Nri/Ui/Tabs/V3.elm
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
module Nri.Ui.Tabs.V3 exposing
|
||||||
|
( Alignment(..)
|
||||||
|
, Config
|
||||||
|
, LinkConfig
|
||||||
|
, Tab
|
||||||
|
, TabLink
|
||||||
|
, links
|
||||||
|
, view
|
||||||
|
, viewCustom
|
||||||
|
, viewTabDefault
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
@docs Alignment
|
||||||
|
@docs Config
|
||||||
|
@docs LinkConfig
|
||||||
|
@docs Tab
|
||||||
|
@docs TabLink
|
||||||
|
@docs links
|
||||||
|
@docs view
|
||||||
|
@docs viewCustom
|
||||||
|
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
@docs viewTabDefault
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Aria
|
||||||
|
import Accessibility.Key
|
||||||
|
import Accessibility.Role
|
||||||
|
import Accessibility.Widget
|
||||||
|
import Css exposing (Style)
|
||||||
|
import Html.Styled as Html exposing (Attribute, Html)
|
||||||
|
import Html.Styled.Attributes as Attributes
|
||||||
|
import Html.Styled.Events as Events
|
||||||
|
import Json.Decode
|
||||||
|
import List.Zipper exposing (Zipper)
|
||||||
|
import List.Zipper.Extra
|
||||||
|
import Nri.Ui.Colors.Extra
|
||||||
|
import Nri.Ui.Colors.V1
|
||||||
|
import Nri.Ui.Fonts.V1
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type alias Config id msg =
|
||||||
|
{ title : Maybe String
|
||||||
|
, onSelect : id -> msg
|
||||||
|
, tabs : Zipper (Tab id)
|
||||||
|
, content : id -> Html msg
|
||||||
|
, alignment : Alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Determines whether tabs are centered or floating to the left or right.
|
||||||
|
-}
|
||||||
|
type Alignment
|
||||||
|
= Left
|
||||||
|
| Center
|
||||||
|
| Right
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type alias Tab id =
|
||||||
|
{ label : String
|
||||||
|
, id : id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
view : Config id msg -> Html msg
|
||||||
|
view config =
|
||||||
|
viewCustom config viewTabDefault
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
viewTabDefault : Tab id -> Html msg
|
||||||
|
viewTabDefault tab =
|
||||||
|
Html.text tab.label
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
viewCustom : Config id msg -> (Tab id -> Html msg) -> Html msg
|
||||||
|
viewCustom config viewInnerTab =
|
||||||
|
let
|
||||||
|
selected =
|
||||||
|
List.Zipper.current config.tabs
|
||||||
|
|
||||||
|
viewTabs =
|
||||||
|
List.Zipper.toList config.tabs
|
||||||
|
|> List.map (viewTab config viewInnerTab selected)
|
||||||
|
in
|
||||||
|
Html.div
|
||||||
|
[]
|
||||||
|
[ Html.styled Html.div
|
||||||
|
[ Css.displayFlex
|
||||||
|
, Css.alignItems Css.flexEnd
|
||||||
|
, Css.borderBottom (Css.px 1)
|
||||||
|
, Css.borderBottomStyle Css.solid
|
||||||
|
, Css.borderBottomColor Nri.Ui.Colors.V1.navy
|
||||||
|
, Nri.Ui.Fonts.V1.baseFont
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ config.title
|
||||||
|
|> Maybe.map viewTitle
|
||||||
|
|> Maybe.withDefault (Html.text "")
|
||||||
|
, Html.styled Html.ul
|
||||||
|
(stylesTabsAligned config.alignment)
|
||||||
|
[ Attributes.fromUnstyled <| Accessibility.Role.tabList
|
||||||
|
]
|
||||||
|
viewTabs
|
||||||
|
]
|
||||||
|
, Html.div
|
||||||
|
[ Attributes.fromUnstyled <| Accessibility.Role.tabPanel
|
||||||
|
, Attributes.fromUnstyled <| Accessibility.Aria.labelledBy (tabToId selected)
|
||||||
|
, Attributes.fromUnstyled <| Accessibility.Widget.hidden False
|
||||||
|
, Attributes.id (tabToBodyId selected)
|
||||||
|
]
|
||||||
|
[ config.content selected.id ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewTitle : String -> Html msg
|
||||||
|
viewTitle title =
|
||||||
|
Html.styled Html.h1
|
||||||
|
[ Css.flexGrow (Css.int 2)
|
||||||
|
, Css.fontSize (Css.px 30)
|
||||||
|
, Css.fontWeight Css.bold
|
||||||
|
, Css.margin Css.zero
|
||||||
|
, Css.marginTop (Css.px 5)
|
||||||
|
, Css.marginBottom (Css.px 10)
|
||||||
|
, Css.color Nri.Ui.Colors.V1.navy
|
||||||
|
, Css.width (Css.px 430)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ Html.text title ]
|
||||||
|
|
||||||
|
|
||||||
|
viewTab : Config id msg -> (Tab id -> Html msg) -> Tab id -> Tab id -> Html msg
|
||||||
|
viewTab { onSelect, tabs } viewInnerTab selected tab =
|
||||||
|
let
|
||||||
|
isSelected =
|
||||||
|
selected.id == tab.id
|
||||||
|
in
|
||||||
|
Html.styled Html.li
|
||||||
|
(stylesTabSelectable isSelected)
|
||||||
|
[ Events.onClick (onSelect tab.id)
|
||||||
|
, Attributes.fromUnstyled <| Accessibility.Key.onKeyDown [ Accessibility.Key.enter (onSelect tab.id) ]
|
||||||
|
, Events.onFocus (onSelect tab.id)
|
||||||
|
, Attributes.tabindex 0
|
||||||
|
, Attributes.fromUnstyled <| Accessibility.Role.presentation
|
||||||
|
, Attributes.id (tabToId tab)
|
||||||
|
, Attributes.fromUnstyled <| Accessibility.Aria.controls (tabToBodyId tab)
|
||||||
|
, Attributes.fromUnstyled <| Accessibility.Widget.selected (selected.id == tab.id)
|
||||||
|
, Events.on "keyup" <|
|
||||||
|
Json.Decode.andThen
|
||||||
|
(\keyCode ->
|
||||||
|
if keyCode == 39 then
|
||||||
|
tabs
|
||||||
|
|> List.Zipper.next
|
||||||
|
|> Maybe.map (List.Zipper.current >> .id >> onSelect >> Json.Decode.succeed)
|
||||||
|
|> Maybe.withDefault (Json.Decode.fail "No next tab")
|
||||||
|
|
||||||
|
else if keyCode == 37 then
|
||||||
|
tabs
|
||||||
|
|> List.Zipper.previous
|
||||||
|
|> Maybe.map (List.Zipper.current >> .id >> onSelect >> Json.Decode.succeed)
|
||||||
|
|> Maybe.withDefault (Json.Decode.fail "No previous tab")
|
||||||
|
|
||||||
|
else
|
||||||
|
Json.Decode.fail "Wrong key code"
|
||||||
|
)
|
||||||
|
Events.keyCode
|
||||||
|
]
|
||||||
|
[ Html.styled Html.div
|
||||||
|
[ Css.color Nri.Ui.Colors.V1.navy
|
||||||
|
, Css.hover [ Css.textDecoration Css.none ]
|
||||||
|
, Css.focus [ Css.textDecoration Css.none ]
|
||||||
|
, Css.display Css.inlineBlock
|
||||||
|
, Css.padding4 (Css.px 14) (Css.px 20) (Css.px 12) (Css.px 20)
|
||||||
|
, Css.position Css.relative
|
||||||
|
, Css.textDecoration Css.none
|
||||||
|
, Css.property "background" "none"
|
||||||
|
, Css.fontFamily Css.inherit
|
||||||
|
, Css.fontSize Css.inherit
|
||||||
|
, Css.cursor Css.pointer
|
||||||
|
]
|
||||||
|
[ Attributes.fromUnstyled <| Accessibility.Role.tab
|
||||||
|
, Attributes.tabindex -1
|
||||||
|
]
|
||||||
|
[ viewInnerTab tab ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Describe a tab that is meant to link to another page
|
||||||
|
-}
|
||||||
|
type alias TabLink =
|
||||||
|
{ label : String
|
||||||
|
, href : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Configure a set a tab links
|
||||||
|
-}
|
||||||
|
type alias LinkConfig msg =
|
||||||
|
{ title : Maybe String
|
||||||
|
, tabs : Zipper TabLink
|
||||||
|
, content : Html msg
|
||||||
|
, alignment : Alignment
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| View a set of tab links
|
||||||
|
-}
|
||||||
|
links : LinkConfig msg -> Html msg
|
||||||
|
links config =
|
||||||
|
Html.div []
|
||||||
|
[ Html.styled Html.nav
|
||||||
|
[ Css.displayFlex
|
||||||
|
, Css.alignItems Css.flexEnd
|
||||||
|
, Css.borderBottom (Css.px 1)
|
||||||
|
, Css.borderBottomStyle Css.solid
|
||||||
|
, Css.borderBottomColor Nri.Ui.Colors.V1.navy
|
||||||
|
, Nri.Ui.Fonts.V1.baseFont
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ config.title
|
||||||
|
|> Maybe.map viewTitle
|
||||||
|
|> Maybe.withDefault (Html.text "")
|
||||||
|
, Html.styled Html.ul
|
||||||
|
(stylesTabsAligned config.alignment)
|
||||||
|
[]
|
||||||
|
(config.tabs
|
||||||
|
|> mapWithCurrent (viewTabLink config)
|
||||||
|
|> List.Zipper.toList
|
||||||
|
)
|
||||||
|
]
|
||||||
|
, Html.div [] [ config.content ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewTabLink : LinkConfig msg -> Bool -> TabLink -> Html msg
|
||||||
|
viewTabLink config isSelected tabLink =
|
||||||
|
Html.styled Html.li
|
||||||
|
(stylesTabSelectable isSelected)
|
||||||
|
[ Attributes.fromUnstyled <| Accessibility.Role.presentation
|
||||||
|
, Attributes.id (tabToId tabLink)
|
||||||
|
]
|
||||||
|
[ case tabLink.href of
|
||||||
|
Just href ->
|
||||||
|
Html.styled Html.a
|
||||||
|
[ Css.color Nri.Ui.Colors.V1.navy
|
||||||
|
, Css.display Css.inlineBlock
|
||||||
|
, Css.padding4 (Css.px 14) (Css.px 20) (Css.px 12) (Css.px 20)
|
||||||
|
, Css.textDecoration Css.none
|
||||||
|
]
|
||||||
|
[ Attributes.href href ]
|
||||||
|
[ Html.text tabLink.label ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Html.styled Html.button
|
||||||
|
[ Css.color Nri.Ui.Colors.V1.navy
|
||||||
|
, Css.display Css.inlineBlock
|
||||||
|
, Css.padding4 (Css.px 14) (Css.px 20) (Css.px 12) (Css.px 20)
|
||||||
|
, Css.textDecoration Css.none
|
||||||
|
, Css.fontFamily Css.inherit
|
||||||
|
, Css.fontSize Css.inherit
|
||||||
|
, Css.border Css.zero
|
||||||
|
, Css.property "background" "none"
|
||||||
|
, Css.lineHeight (Css.num 1)
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
[ Html.text tabLink.label ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HELP
|
||||||
|
|
||||||
|
|
||||||
|
tabToId : { a | label : String } -> String
|
||||||
|
tabToId tab =
|
||||||
|
tab.label
|
||||||
|
|
||||||
|
|
||||||
|
tabToBodyId : { a | label : String } -> String
|
||||||
|
tabToBodyId tab =
|
||||||
|
"tab-body-" ++ tab.label
|
||||||
|
|
||||||
|
|
||||||
|
mapWithCurrent : (Bool -> a -> b) -> Zipper a -> Zipper b
|
||||||
|
mapWithCurrent fn zipper =
|
||||||
|
List.Zipper.Extra.from
|
||||||
|
(List.map (fn False) (List.Zipper.before zipper))
|
||||||
|
(fn True (List.Zipper.current zipper))
|
||||||
|
(List.map (fn False) (List.Zipper.after zipper))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- STYLES
|
||||||
|
|
||||||
|
|
||||||
|
stylesTabsAligned : Alignment -> List Style
|
||||||
|
stylesTabsAligned alignment =
|
||||||
|
let
|
||||||
|
alignmentStyles =
|
||||||
|
case alignment of
|
||||||
|
Left ->
|
||||||
|
[ Css.justifyContent Css.flexStart ]
|
||||||
|
|
||||||
|
Center ->
|
||||||
|
[ Css.justifyContent Css.center ]
|
||||||
|
|
||||||
|
Right ->
|
||||||
|
[ Css.justifyContent Css.flexEnd ]
|
||||||
|
in
|
||||||
|
stylesTabs ++ alignmentStyles
|
||||||
|
|
||||||
|
|
||||||
|
stylesTabs : List Style
|
||||||
|
stylesTabs =
|
||||||
|
[ Css.listStyle Css.none
|
||||||
|
, Css.margin Css.zero
|
||||||
|
, Css.fontSize (Css.px 19)
|
||||||
|
, Css.displayFlex
|
||||||
|
, Css.flexGrow (Css.int 1)
|
||||||
|
, Css.marginRight (Css.px 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
stylesTabSelectable : Bool -> List Style
|
||||||
|
stylesTabSelectable isSelected =
|
||||||
|
let
|
||||||
|
stylesDynamic =
|
||||||
|
if isSelected then
|
||||||
|
[ Css.backgroundColor Nri.Ui.Colors.V1.white
|
||||||
|
, Css.borderBottom (Css.px 1)
|
||||||
|
, Css.borderBottomStyle Css.solid
|
||||||
|
, Css.borderBottomColor Nri.Ui.Colors.V1.white
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
[ Css.backgroundColor Nri.Ui.Colors.V1.frost
|
||||||
|
, Css.backgroundImage <|
|
||||||
|
Css.linearGradient2 Css.toTop
|
||||||
|
(Css.stop2 (Nri.Ui.Colors.Extra.withAlpha 0.25 Nri.Ui.Colors.V1.azure) (Css.pct 0))
|
||||||
|
(Css.stop2 (Nri.Ui.Colors.Extra.withAlpha 0 Nri.Ui.Colors.V1.azure) (Css.pct 25))
|
||||||
|
[ Css.stop2 (Nri.Ui.Colors.Extra.withAlpha 0 Nri.Ui.Colors.V1.azure) (Css.pct 100) ]
|
||||||
|
]
|
||||||
|
in
|
||||||
|
stylesTab ++ stylesDynamic
|
||||||
|
|
||||||
|
|
||||||
|
stylesTab : List Style
|
||||||
|
stylesTab =
|
||||||
|
[ Css.display Css.inlineBlock
|
||||||
|
, Css.borderTopLeftRadius (Css.px 10)
|
||||||
|
, Css.borderTopRightRadius (Css.px 10)
|
||||||
|
, Css.border3 (Css.px 1) Css.solid Nri.Ui.Colors.V1.navy
|
||||||
|
, Css.marginBottom (Css.px -1)
|
||||||
|
, Css.marginLeft (Css.px 10)
|
||||||
|
, Css.cursor Css.pointer
|
||||||
|
, Css.firstChild
|
||||||
|
[ Css.marginLeft Css.zero
|
||||||
|
]
|
||||||
|
]
|
202
src/Nri/Ui/TextArea/V3.elm
Normal file
202
src/Nri/Ui/TextArea/V3.elm
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
module Nri.Ui.TextArea.V3 exposing (view, writing, contentCreation, Height(..), HeightBehavior(..), Model, generateId)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
## Upgrading to V3
|
||||||
|
|
||||||
|
- Do nothing! (This just uses new elm-css styles)
|
||||||
|
|
||||||
|
|
||||||
|
## The Nri styleguide-specified textarea with overlapping label
|
||||||
|
|
||||||
|
@docs view, writing, contentCreation, Height, HeightBehavior, Model, generateId
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Accessibility.Styled.Style
|
||||||
|
import Css exposing (plus, px)
|
||||||
|
import Html.Styled as Html exposing (Html)
|
||||||
|
import Html.Styled.Attributes as Attributes
|
||||||
|
import Html.Styled.Events as Events
|
||||||
|
import Nri.Ui.InputStyles.V2 as InputStyles
|
||||||
|
exposing
|
||||||
|
( Theme(..)
|
||||||
|
, input
|
||||||
|
, label
|
||||||
|
)
|
||||||
|
import Nri.Ui.Util exposing (dashify, removePunctuation)
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
type alias Model msg =
|
||||||
|
{ value : String
|
||||||
|
, autofocus : Bool
|
||||||
|
, onInput : String -> msg
|
||||||
|
, isInError : Bool
|
||||||
|
, height : HeightBehavior
|
||||||
|
, placeholder : String
|
||||||
|
, label : String
|
||||||
|
, showLabel : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Control whether to auto-expand the height.
|
||||||
|
-}
|
||||||
|
type HeightBehavior
|
||||||
|
= Fixed
|
||||||
|
| AutoResize Height
|
||||||
|
|
||||||
|
|
||||||
|
{-| For specifying the actual height.
|
||||||
|
-}
|
||||||
|
type Height
|
||||||
|
= DefaultHeight
|
||||||
|
| SingleLine
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
view : Model msg -> Html msg
|
||||||
|
view model =
|
||||||
|
view_ Standard model
|
||||||
|
|
||||||
|
|
||||||
|
{-| Used for Writing Cycles
|
||||||
|
-}
|
||||||
|
writing : Model msg -> Html msg
|
||||||
|
writing model =
|
||||||
|
view_ Writing model
|
||||||
|
|
||||||
|
|
||||||
|
{-| Used for Content Creation
|
||||||
|
-}
|
||||||
|
contentCreation : Model msg -> Html msg
|
||||||
|
contentCreation model =
|
||||||
|
view_ ContentCreation model
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
view_ : Theme -> Model msg -> Html msg
|
||||||
|
view_ theme model =
|
||||||
|
let
|
||||||
|
autoresizeAttrs =
|
||||||
|
case model.height of
|
||||||
|
AutoResize _ ->
|
||||||
|
[ Attributes.attribute "data-autoresize" "" ]
|
||||||
|
|
||||||
|
Fixed ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
heightForStyle =
|
||||||
|
case theme of
|
||||||
|
Standard ->
|
||||||
|
InputStyles.textAreaHeight
|
||||||
|
|
||||||
|
ContentCreation ->
|
||||||
|
InputStyles.textAreaHeight
|
||||||
|
|
||||||
|
Writing ->
|
||||||
|
InputStyles.writingMinHeight
|
||||||
|
in
|
||||||
|
Html.styled Html.div
|
||||||
|
[ Css.position Css.relative ]
|
||||||
|
[]
|
||||||
|
[ Html.styled (Html.node "nri-textarea-v3")
|
||||||
|
[ Css.display Css.block ]
|
||||||
|
autoresizeAttrs
|
||||||
|
[ Html.styled Html.textarea
|
||||||
|
[ InputStyles.input theme model.isInError
|
||||||
|
, Css.boxSizing Css.borderBox
|
||||||
|
, case model.height of
|
||||||
|
AutoResize minimumHeight ->
|
||||||
|
Css.minHeight (calculateMinHeight theme minimumHeight)
|
||||||
|
|
||||||
|
Fixed ->
|
||||||
|
Css.minHeight heightForStyle
|
||||||
|
]
|
||||||
|
[ Events.onInput model.onInput
|
||||||
|
, Attributes.value model.value
|
||||||
|
, Attributes.id (generateId model.label)
|
||||||
|
, Attributes.autofocus model.autofocus
|
||||||
|
, Attributes.placeholder model.placeholder
|
||||||
|
, Attributes.attribute "data-gramm" "false" -- disables grammarly to prevent https://github.com/NoRedInk/NoRedInk/issues/14859
|
||||||
|
, Attributes.class "override-sass-styles"
|
||||||
|
, Attributes.attribute "aria-invalid" <|
|
||||||
|
if model.isInError then
|
||||||
|
"true"
|
||||||
|
|
||||||
|
else
|
||||||
|
"false"
|
||||||
|
]
|
||||||
|
[]
|
||||||
|
, if not model.showLabel then
|
||||||
|
Html.label
|
||||||
|
([ Attributes.for (generateId model.label)
|
||||||
|
]
|
||||||
|
++ Accessibility.Styled.Style.invisible
|
||||||
|
)
|
||||||
|
[ Html.text model.label ]
|
||||||
|
|
||||||
|
else
|
||||||
|
Html.label
|
||||||
|
[ Attributes.for (generateId model.label)
|
||||||
|
, Attributes.css [ InputStyles.label theme model.isInError ]
|
||||||
|
]
|
||||||
|
[ Html.text model.label ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
calculateMinHeight : Theme -> Height -> Css.Px
|
||||||
|
calculateMinHeight textAreaStyle specifiedHeight =
|
||||||
|
{- On including padding in this calculation:
|
||||||
|
|
||||||
|
When the textarea is autoresized, TextArea.js updates the textarea's
|
||||||
|
height by taking its scrollHeight. Because scrollHeight's calculation
|
||||||
|
includes the element's padding no matter what [1], we need to set the
|
||||||
|
textarea's box-sizing to border-box in order to use the same measurement
|
||||||
|
for its height as scrollHeight.
|
||||||
|
|
||||||
|
So, min-height also needs to be specified in terms of padding + content
|
||||||
|
height.
|
||||||
|
|
||||||
|
[1] https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
||||||
|
-}
|
||||||
|
case specifiedHeight of
|
||||||
|
SingleLine ->
|
||||||
|
case textAreaStyle of
|
||||||
|
Standard ->
|
||||||
|
singleLineHeight
|
||||||
|
|
||||||
|
Writing ->
|
||||||
|
writingSingleLineHeight
|
||||||
|
|
||||||
|
ContentCreation ->
|
||||||
|
singleLineHeight
|
||||||
|
|
||||||
|
DefaultHeight ->
|
||||||
|
case textAreaStyle of
|
||||||
|
Standard ->
|
||||||
|
InputStyles.textAreaHeight
|
||||||
|
|
||||||
|
Writing ->
|
||||||
|
InputStyles.writingMinHeight
|
||||||
|
|
||||||
|
ContentCreation ->
|
||||||
|
InputStyles.textAreaHeight
|
||||||
|
|
||||||
|
|
||||||
|
singleLineHeight : Css.Px
|
||||||
|
singleLineHeight =
|
||||||
|
px (.numericValue InputStyles.inputPaddingVertical + .numericValue InputStyles.inputLineHeight + .numericValue InputStyles.inputPaddingVertical)
|
||||||
|
|
||||||
|
|
||||||
|
writingSingleLineHeight : Css.Px
|
||||||
|
writingSingleLineHeight =
|
||||||
|
px (.numericValue InputStyles.writingPaddingTop + .numericValue InputStyles.writingLineHeight + .numericValue InputStyles.writingPadding)
|
||||||
|
|
||||||
|
|
||||||
|
{-| -}
|
||||||
|
generateId : String -> String
|
||||||
|
generateId labelText =
|
||||||
|
"nri-ui-text-area-" ++ (dashify <| removePunctuation labelText)
|
@ -3,11 +3,19 @@
|
|||||||
"tests": [
|
"tests": [
|
||||||
"Nri.Ui",
|
"Nri.Ui",
|
||||||
"Nri.Ui.Accordion.V1",
|
"Nri.Ui.Accordion.V1",
|
||||||
|
"Nri.Ui.Alert.V2",
|
||||||
|
"Nri.Ui.Alert.V3",
|
||||||
"Nri.Ui.Alert.V4",
|
"Nri.Ui.Alert.V4",
|
||||||
"Nri.Ui.AssetPath",
|
"Nri.Ui.AssetPath",
|
||||||
"Nri.Ui.AssignmentIcon.V1",
|
"Nri.Ui.AssignmentIcon.V1",
|
||||||
|
"Nri.Ui.BannerAlert.V2",
|
||||||
|
"Nri.Ui.BannerAlert.V3",
|
||||||
"Nri.Ui.BannerAlert.V6",
|
"Nri.Ui.BannerAlert.V6",
|
||||||
|
"Nri.Ui.Button.V3",
|
||||||
|
"Nri.Ui.Button.V4",
|
||||||
"Nri.Ui.Button.V5",
|
"Nri.Ui.Button.V5",
|
||||||
|
"Nri.Ui.Button.V6",
|
||||||
|
"Nri.Ui.Button.V7",
|
||||||
"Nri.Ui.Button.V8",
|
"Nri.Ui.Button.V8",
|
||||||
"Nri.Ui.Button.V10",
|
"Nri.Ui.Button.V10",
|
||||||
"Nri.Ui.Callout.V1",
|
"Nri.Ui.Callout.V1",
|
||||||
@ -25,6 +33,7 @@
|
|||||||
"Nri.Ui.Dropdown.V2",
|
"Nri.Ui.Dropdown.V2",
|
||||||
"Nri.Ui.Effects.V1",
|
"Nri.Ui.Effects.V1",
|
||||||
"Nri.Ui.Fonts.V1",
|
"Nri.Ui.Fonts.V1",
|
||||||
|
"Nri.Ui.Heading.V1",
|
||||||
"Nri.Ui.Heading.V2",
|
"Nri.Ui.Heading.V2",
|
||||||
"Nri.Ui.Html.Attributes.V2",
|
"Nri.Ui.Html.Attributes.V2",
|
||||||
"Nri.Ui.Html.V3",
|
"Nri.Ui.Html.V3",
|
||||||
@ -34,7 +43,10 @@
|
|||||||
"Nri.Ui.InputStyles.V2",
|
"Nri.Ui.InputStyles.V2",
|
||||||
"Nri.Ui.Logo.V1",
|
"Nri.Ui.Logo.V1",
|
||||||
"Nri.Ui.MasteryIcon.V1",
|
"Nri.Ui.MasteryIcon.V1",
|
||||||
|
"Nri.Ui.Modal.V2",
|
||||||
|
"Nri.Ui.Modal.V3",
|
||||||
"Nri.Ui.Modal.V8",
|
"Nri.Ui.Modal.V8",
|
||||||
|
"Nri.Ui.Page.V2",
|
||||||
"Nri.Ui.Page.V3",
|
"Nri.Ui.Page.V3",
|
||||||
"Nri.Ui.Palette.V1",
|
"Nri.Ui.Palette.V1",
|
||||||
"Nri.Ui.Pennant.V2",
|
"Nri.Ui.Pennant.V2",
|
||||||
@ -42,6 +54,7 @@
|
|||||||
"Nri.Ui.PremiumCheckbox.V2",
|
"Nri.Ui.PremiumCheckbox.V2",
|
||||||
"Nri.Ui.PremiumCheckbox.V3",
|
"Nri.Ui.PremiumCheckbox.V3",
|
||||||
"Nri.Ui.PremiumCheckbox.V6",
|
"Nri.Ui.PremiumCheckbox.V6",
|
||||||
|
"Nri.Ui.SegmentedControl.V6",
|
||||||
"Nri.Ui.SegmentedControl.V7",
|
"Nri.Ui.SegmentedControl.V7",
|
||||||
"Nri.Ui.SegmentedControl.V8",
|
"Nri.Ui.SegmentedControl.V8",
|
||||||
"Nri.Ui.Select.V5",
|
"Nri.Ui.Select.V5",
|
||||||
@ -56,9 +69,11 @@
|
|||||||
"Nri.Ui.Table.V4",
|
"Nri.Ui.Table.V4",
|
||||||
"Nri.Ui.Table.V5",
|
"Nri.Ui.Table.V5",
|
||||||
"Nri.Ui.Tabs.V4",
|
"Nri.Ui.Tabs.V4",
|
||||||
|
"Nri.Ui.Tabs.V3",
|
||||||
"Nri.Ui.Text.V2",
|
"Nri.Ui.Text.V2",
|
||||||
"Nri.Ui.Text.V4",
|
"Nri.Ui.Text.V4",
|
||||||
"Nri.Ui.Text.Writing.V1",
|
"Nri.Ui.Text.Writing.V1",
|
||||||
|
"Nri.Ui.TextArea.V3",
|
||||||
"Nri.Ui.TextArea.V4",
|
"Nri.Ui.TextArea.V4",
|
||||||
"Nri.Ui.TextInput.V3",
|
"Nri.Ui.TextInput.V3",
|
||||||
"Nri.Ui.TextInput.V4",
|
"Nri.Ui.TextInput.V4",
|
||||||
|
Loading…
Reference in New Issue
Block a user