noredink-ui/styleguide-app/App.elm

406 lines
12 KiB
Elm
Raw Normal View History

module App exposing (Effect(..), Model, Msg(..), init, perform, subscriptions, update, view)
2018-02-13 00:32:38 +03:00
2021-11-05 21:26:43 +03:00
import Accessibility.Styled as Html exposing (Html)
import Accessibility.Styled.Key as Key
import Browser exposing (Document, UrlRequest(..))
2020-03-24 22:26:41 +03:00
import Browser.Dom
import Browser.Navigation exposing (Key)
2022-03-30 21:15:14 +03:00
import Category exposing (Category)
import Css exposing (..)
import Css.Media exposing (withMedia)
import Dict exposing (Dict)
import Example exposing (Example)
import Examples
2022-03-15 21:06:13 +03:00
import Html.Styled.Attributes exposing (..)
2022-03-29 00:50:12 +03:00
import Http
import Json.Decode as Decode
2020-11-06 21:51:33 +03:00
import Nri.Ui.CssVendorPrefix.V1 as VendorPrefixed
import Nri.Ui.Heading.V2 as Heading
2022-03-29 23:43:24 +03:00
import Nri.Ui.MediaQuery.V1 exposing (mobile)
import Nri.Ui.Page.V3 as Page
2022-03-29 23:43:24 +03:00
import Nri.Ui.SideNav.V3 as SideNav
import Nri.Ui.Sprite.V1 as Sprite
2022-05-10 01:36:47 +03:00
import Nri.Ui.UiIcon.V1 as UiIcon
import Routes
2022-03-15 21:06:13 +03:00
import Sort.Set as Set
2020-03-24 22:26:41 +03:00
import Task
import Url exposing (Url)
2018-02-13 00:32:38 +03:00
type alias Route =
Routes.Route Examples.State Examples.Msg
type alias Model key =
{ -- Global UI
route : Route
, previousRoute : Maybe Route
, moduleStates : Dict String (Example Examples.State Examples.Msg)
, navigationKey : key
, elliePackageDependencies : Result Http.Error (Dict String String)
}
init : () -> Url -> key -> ( Model key, Effect )
2018-12-05 21:56:04 +03:00
init () url key =
let
moduleStates =
Dict.fromList
(List.map (\example -> ( example.name, example )) Examples.all)
in
( { route = Routes.fromLocation moduleStates url
, previousRoute = Nothing
, moduleStates = moduleStates
2018-12-05 21:56:04 +03:00
, navigationKey = key
, elliePackageDependencies = Ok Dict.empty
2018-02-13 00:32:38 +03:00
}
2022-03-29 00:50:12 +03:00
, Cmd.batch
[ loadPackage
, loadApplicationDependencies
]
|> Command
2018-02-13 00:32:38 +03:00
)
type Msg
= UpdateModuleStates String Examples.Msg
| OnUrlRequest Browser.UrlRequest
| OnUrlChange Url
| ChangeRoute Route
2020-03-24 22:26:41 +03:00
| SkipToMainContent
2022-03-29 00:50:12 +03:00
| LoadedPackages (Result Http.Error (Dict String String))
| Focused (Result Browser.Dom.Error ())
update : Msg -> Model key -> ( Model key, Effect )
update action model =
case action of
UpdateModuleStates key exampleMsg ->
case Dict.get key model.moduleStates of
Just example ->
example.update exampleMsg example.state
|> Tuple.mapFirst
(\newState ->
2022-05-22 04:27:46 +03:00
let
newExample =
{ example | state = newState }
in
{ model
2022-05-22 04:27:46 +03:00
| moduleStates = Dict.insert key newExample model.moduleStates
, route =
Maybe.withDefault model.route
(Routes.updateExample newExample model.route)
}
)
|> Tuple.mapSecond (Cmd.map (UpdateModuleStates key) >> Command)
Nothing ->
( model, None )
OnUrlRequest request ->
case request of
Internal loc ->
( model, GoToUrl loc )
External loc ->
( model, Load loc )
2022-05-23 19:58:21 +03:00
OnUrlChange location ->
let
route =
Routes.fromLocation model.moduleStates location
in
( { model | route = route, previousRoute = Just model.route }
, Maybe.map FocusOn (Routes.headerId route)
|> Maybe.withDefault None
)
ChangeRoute route ->
( model
, GoToRoute route
)
2020-03-24 22:26:41 +03:00
SkipToMainContent ->
( model
, FocusOn "maincontent"
2020-03-24 22:26:41 +03:00
)
2022-03-29 00:52:06 +03:00
LoadedPackages newPackagesResult ->
2022-03-29 20:31:06 +03:00
let
-- Ellie gets really slow to compile if we include all the packages, unfortunately!
-- feel free to adjust the settings here if you need more packages for a particular example.
removedPackages =
[ "avh4/elm-debug-controls"
, "BrianHicks/elm-particle"
, "elm-community/random-extra"
, "elm/browser"
, "elm/http"
, "elm/json"
, "elm/parser"
, "elm/random"
, "elm/regex"
, "elm/svg"
, "elm/url"
, "elm-community/string-extra"
, "Gizra/elm-keyboard-event"
, "pablohirafuji/elm-markdown"
, "rtfeldman/elm-sorter-experiment"
, "tesk9/accessible-html-with-css"
, "tesk9/palette"
, "wernerdegroot/listzipper"
]
in
2022-03-29 00:52:06 +03:00
( { model
| elliePackageDependencies =
2022-03-29 20:31:06 +03:00
List.foldl (\name -> Result.map (Dict.remove name))
(Result.map2 Dict.union model.elliePackageDependencies newPackagesResult)
removedPackages
2022-03-29 00:52:06 +03:00
}
, None
2022-03-29 00:50:12 +03:00
)
Focused _ ->
( model, None )
type Effect
= GoToRoute Route
| GoToUrl Url
| Load String
| FocusOn String
| None
| Command (Cmd Msg)
perform : Key -> Effect -> Cmd Msg
perform navigationKey effect =
case effect of
GoToRoute route ->
Browser.Navigation.pushUrl navigationKey (Routes.toString route)
GoToUrl url ->
Browser.Navigation.pushUrl navigationKey (Url.toString url)
Load loc ->
Browser.Navigation.load loc
FocusOn id ->
Task.attempt Focused (Browser.Dom.focus id)
None ->
Cmd.none
Command cmd ->
cmd
subscriptions : Model key -> Sub Msg
subscriptions model =
Dict.values model.moduleStates
|> List.map (\example -> Sub.map (UpdateModuleStates example.name) (example.subscriptions example.state))
|> Sub.batch
view : Model key -> Document Msg
view model =
2020-06-19 23:52:02 +03:00
let
toBody view_ =
List.map Html.toUnstyled
[ view_
, Html.map never Sprite.attach
]
in
case model.route of
Routes.Doodad example ->
{ title = example.name ++ " in the NoRedInk Style Guide"
, body = viewExample model example |> toBody
}
2022-05-20 23:47:40 +03:00
Routes.CategoryDoodad _ example ->
{ title = example.name ++ " in the NoRedInk Style Guide"
, body = viewExample model example |> toBody
}
Routes.NotFound name ->
{ title = name ++ " was not found in the NoRedInk Style Guide"
, body = toBody notFound
}
Routes.Category category ->
2022-03-30 21:24:26 +03:00
{ title = Category.forDisplay category ++ " Category in the NoRedInk Style Guide"
, body = toBody (viewCategory model category)
}
Routes.All ->
2022-03-30 21:24:26 +03:00
{ title = "NoRedInk Style Guide"
, body = toBody (viewAll model)
}
viewExample : Model key -> Example a Examples.Msg -> Html Msg
2022-03-30 21:01:16 +03:00
viewExample model example =
Example.view { packageDependencies = model.elliePackageDependencies } example
|> Html.map (UpdateModuleStates example.name)
|> withSideNav model
2022-03-30 21:01:16 +03:00
2022-03-30 21:13:11 +03:00
notFound : Html Msg
notFound =
Page.notFound
{ link = ChangeRoute Routes.All
, recoveryText = Page.ReturnTo "Component Library"
}
viewAll : Model key -> Html Msg
2022-03-30 21:15:58 +03:00
viewAll model =
withSideNav model <|
2022-05-20 23:47:40 +03:00
viewPreviews "all"
{ navigate = Routes.Doodad >> ChangeRoute
, exampleHref = Routes.Doodad >> Routes.toString
}
(Dict.values model.moduleStates)
2022-03-30 21:15:58 +03:00
viewCategory : Model key -> Category -> Html Msg
2022-03-30 21:15:14 +03:00
viewCategory model category =
withSideNav model
2022-05-20 22:20:12 +03:00
(model.moduleStates
2022-03-30 21:15:14 +03:00
|> Dict.values
|> List.filter
(\doodad ->
Set.memberOf
(Set.fromList Category.sorter doodad.categories)
category
)
|> viewPreviews (Category.forId category)
2022-05-20 23:47:40 +03:00
{ navigate = Routes.CategoryDoodad category >> ChangeRoute
, exampleHref = Routes.CategoryDoodad category >> Routes.toString
}
2022-05-20 22:20:12 +03:00
)
2022-03-30 21:15:14 +03:00
withSideNav :
{ model | route : Route, moduleStates : Dict String (Example Examples.State Examples.Msg) }
-> Html Msg
-> Html Msg
withSideNav model content =
2020-06-20 00:12:53 +03:00
Html.div
[ css
[ displayFlex
, withMedia [ mobile ] [ flexDirection column, alignItems stretch ]
2020-06-20 00:12:53 +03:00
, alignItems flexStart
2021-12-03 22:40:57 +03:00
, maxWidth (Css.px 1400)
, margin auto
2020-06-20 00:12:53 +03:00
]
]
[ navigation model
2021-11-05 21:26:43 +03:00
, Html.main_
[ css
[ flexGrow (int 1)
, margin2 (px 40) zero
, Css.minHeight (Css.vh 100)
]
, id "maincontent"
, Key.tabbable False
2021-11-05 21:26:43 +03:00
]
2022-05-20 22:20:12 +03:00
[ Html.div [ css [ Css.marginBottom (Css.px 30) ] ]
[ Routes.viewBreadCrumbs model.route
2022-05-20 22:20:12 +03:00
]
, content
]
]
2022-05-20 23:47:40 +03:00
viewPreviews :
String
->
{ navigate : Example Examples.State Examples.Msg -> Msg
, exampleHref : Example Examples.State Examples.Msg -> String
}
-> List (Example Examples.State Examples.Msg)
-> Html Msg
viewPreviews containerId navConfig examples =
examples
2022-05-20 23:47:40 +03:00
|> List.map (Example.preview navConfig)
|> Html.div
[ id containerId
, css
[ Css.displayFlex
, Css.flexWrap Css.wrap
, Css.property "gap" "10px"
]
]
navigation :
{ model | route : Route, moduleStates : Dict String (Example Examples.State Examples.Msg) }
-> Html Msg
navigation { moduleStates, route } =
let
examples =
Dict.values moduleStates
exampleEntriesForCategory category =
List.filter (\{ categories } -> List.any ((==) category) categories) examples
|> List.map
(\example ->
SideNav.entry example.name
[ SideNav.href (Routes.CategoryDoodad category example)
]
)
2021-12-04 00:34:32 +03:00
categoryNavLinks : List (SideNav.Entry Route Msg)
categoryNavLinks =
List.map
(\category ->
SideNav.entryWithChildren (Category.forDisplay category)
[ SideNav.href (Routes.Category category)
]
(exampleEntriesForCategory category)
2021-12-04 00:34:32 +03:00
)
Category.all
in
2021-12-03 19:45:06 +03:00
SideNav.view
{ isCurrentRoute = (==) route
, routeToString = Routes.toString
2021-12-03 19:45:06 +03:00
, onSkipNav = SkipToMainContent
}
2022-03-29 23:43:24 +03:00
[ SideNav.navNotMobileCss
[ VendorPrefixed.value "position" "sticky"
, top (px 55)
]
]
2022-05-10 19:50:07 +03:00
(SideNav.entry "Usage Guidelines"
2022-05-10 01:36:47 +03:00
[ SideNav.linkExternal "https://paper.dropbox.com/doc/UI-Style-Guide-and-Caveats--BhJHYronm1RGM1hRfnkvhrZMAg-PvOLxeX3oyujYEzdJx5pu"
, SideNav.icon UiIcon.openInNewTab
]
:: SideNav.entry "All" [ SideNav.href Routes.All ]
2021-12-04 00:34:32 +03:00
:: categoryNavLinks
)
2022-03-29 00:50:12 +03:00
loadPackage : Cmd Msg
loadPackage =
Http.get
{ url = "/package.json"
, expect =
Http.expectJson
LoadedPackages
(Decode.map2 Dict.singleton
(Decode.field "name" Decode.string)
(Decode.field "version" Decode.string)
)
}
loadApplicationDependencies : Cmd Msg
loadApplicationDependencies =
Http.get
{ url = "/application.json"
, expect =
Http.expectJson
LoadedPackages
(Decode.at [ "dependencies", "direct" ] (Decode.dict Decode.string))
}