noredink-ui/styleguide-app/Main.elm
2022-03-30 11:15:58 -07:00

342 lines
9.8 KiB
Elm

module Main exposing (main)
import Accessibility.Styled as Html exposing (Html)
import Browser exposing (Document, UrlRequest(..))
import Browser.Dom
import Browser.Navigation exposing (Key)
import Category exposing (Category)
import Css exposing (..)
import Css.Media exposing (withMedia)
import Dict exposing (Dict)
import Example exposing (Example)
import Examples
import Html.Styled.Attributes exposing (..)
import Http
import Json.Decode as Decode
import Nri.Ui.CssVendorPrefix.V1 as VendorPrefixed
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.MediaQuery.V1 exposing (mobile, notMobile)
import Nri.Ui.Page.V3 as Page
import Nri.Ui.SideNav.V2 as SideNav
import Nri.Ui.Sprite.V1 as Sprite
import Routes exposing (Route)
import Sort.Set as Set
import Task
import Url exposing (Url)
main : Program () Model Msg
main =
Browser.application
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
, onUrlRequest = OnUrlRequest
, onUrlChange = OnUrlChange
}
type alias Model =
{ -- 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, Cmd Msg )
init () url key =
( { route = Routes.fromLocation url
, previousRoute = Nothing
, moduleStates =
Dict.fromList
(List.map (\example -> ( example.name, example )) Examples.all)
, navigationKey = key
, elliePackageDependencies = Ok Dict.empty
}
, Cmd.batch
[ loadPackage
, loadApplicationDependencies
]
)
type Msg
= UpdateModuleStates String Examples.Msg
| OnUrlRequest Browser.UrlRequest
| OnUrlChange Url
| ChangeRoute Route
| SkipToMainContent
| LoadedPackages (Result Http.Error (Dict String String))
| NoOp
update : Msg -> Model -> ( Model, Cmd Msg )
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 ->
{ model
| moduleStates =
Dict.insert key
{ example | state = newState }
model.moduleStates
}
)
|> Tuple.mapSecond (Cmd.map (UpdateModuleStates key))
Nothing ->
( model, Cmd.none )
OnUrlRequest request ->
case request of
Internal loc ->
( model, Browser.Navigation.pushUrl model.navigationKey (Url.toString loc) )
External loc ->
( model, Browser.Navigation.load loc )
OnUrlChange route ->
( { model
| route = Routes.fromLocation route
, previousRoute = Just model.route
}
, Cmd.none
)
ChangeRoute route ->
( model
, Browser.Navigation.pushUrl model.navigationKey
(Routes.toString route)
)
SkipToMainContent ->
( model
, Task.attempt (\_ -> NoOp) (Browser.Dom.focus "maincontent")
)
LoadedPackages newPackagesResult ->
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
( { model
| elliePackageDependencies =
List.foldl (\name -> Result.map (Dict.remove name))
(Result.map2 Dict.union model.elliePackageDependencies newPackagesResult)
removedPackages
}
, Cmd.none
)
NoOp ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Dict.values model.moduleStates
|> List.map (\example -> Sub.map (UpdateModuleStates example.name) (example.subscriptions example.state))
|> Sub.batch
view : Model -> Document Msg
view model =
{ title = "Style Guide"
, body =
[ view_ model |> Html.toUnstyled
, Sprite.attach |> Html.map never |> Html.toUnstyled
]
}
view_ : Model -> Html Msg
view_ model =
let
examples filterBy =
List.filter (\m -> filterBy m) (Dict.values model.moduleStates)
in
case model.route of
Routes.Doodad doodad ->
case List.head (examples (\m -> m.name == doodad)) of
Just example ->
viewExample model example
|> Html.map (UpdateModuleStates example.name)
Nothing ->
notFound
Routes.Category category ->
viewCategory model category
Routes.All ->
viewAll model
viewExample : Model -> Example a msg -> Html msg
viewExample model example =
Html.div [ css [ maxWidth (Css.px 1400), margin auto ] ]
[ Example.view model.previousRoute
{ packageDependencies = model.elliePackageDependencies }
example
]
notFound : Html Msg
notFound =
Page.notFound
{ link = ChangeRoute Routes.All
, recoveryText = Page.ReturnTo "Component Library"
}
viewAll : Model -> Html Msg
viewAll model =
withSideNav model.route
[ mainContentHeader "All"
, viewPreviews "all" (Dict.values model.moduleStates)
]
viewCategory : Model -> Category -> Html Msg
viewCategory model category =
withSideNav model.route
[ mainContentHeader (Category.forDisplay category)
, model.moduleStates
|> Dict.values
|> List.filter
(\doodad ->
Set.memberOf
(Set.fromList Category.sorter doodad.categories)
category
)
|> viewPreviews (Category.forId category)
]
withSideNav : Route -> List (Html Msg) -> Html Msg
withSideNav currentRoute content =
Html.div
[ css
[ displayFlex
, withMedia [ mobile ] [ flexDirection column, alignItems stretch ]
, alignItems flexStart
, maxWidth (Css.px 1400)
, margin auto
]
]
[ navigation currentRoute
, Html.main_
[ css
[ flexGrow (int 1)
, margin2 (px 40) zero
, Css.minHeight (Css.vh 100)
]
]
content
]
mainContentHeader : String -> Html msg
mainContentHeader heading =
Heading.h1
[ Heading.customAttr (id "maincontent")
, Heading.customAttr (tabindex -1)
, Heading.css [ marginBottom (px 30) ]
]
[ Html.text heading ]
viewPreviews : String -> List (Example state msg) -> Html Msg
viewPreviews containerId examples =
examples
|> List.map (\example -> Example.preview ChangeRoute example)
|> Html.div
[ id containerId
, css
[ Css.displayFlex
, Css.flexWrap Css.wrap
, Css.property "gap" "10px"
]
]
navigation : Route -> Html Msg
navigation currentRoute =
let
categoryNavLinks : List (SideNav.Entry Route Msg)
categoryNavLinks =
List.map
(\category ->
SideNav.entry (Category.forDisplay category)
[ SideNav.href (Routes.Category category) ]
)
Category.all
in
SideNav.view
{ isCurrentRoute = (==) currentRoute
, routeToString = Routes.toString
, onSkipNav = SkipToMainContent
, css =
[ withMedia [ notMobile ]
[ VendorPrefixed.value "position" "sticky"
, top (px 55)
]
]
}
(SideNav.entry "All" [ SideNav.href Routes.All ]
:: categoryNavLinks
)
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))
}