From c90847b8f4e944c4c8219bac6faf26e95a68529c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8jberg?= Date: Fri, 7 May 2021 16:35:34 -0400 Subject: [PATCH] Add a Keyboard Shortcuts help modal * Enable the user to click "Keyboard Shortcuts" in the lower left of the sidebar or press "?" to open the new HelpModal which (for now, only) includes the keyboard shortcut list * Add a System model, companioned by a JavaScript module for detecting the Operating System of the client to allow for "Meta" to be translated to the Windows key and Cmd key appropriately * Disable Meta+k for any Operating System thats not macOS * Render Meta Key correctly depending on Operating System * Fix a modal issue in Firefox where modals where full height instead of fitted to their content. * Update the KeyboardShortcut module to support a modal compositional way to construct shortcut indicators if needed * Refactor the Modal module to better support different kinds of modals with clear defaults (Finder is a uniquely styled modal and the new Help modal will be default style). --- package.json | 2 +- src/App.elm | 140 +++++++++++++++++--- src/Finder.elm | 63 +++++---- src/Hub.elm | 2 +- src/KeyboardShortcut.elm | 90 +++++++++---- src/KeyboardShortcut/Key.elm | 243 +++++++++++++++-------------------- src/System.elm | 44 +++++++ src/UI.elm | 5 + src/UI/Modal.elm | 87 +++++++++++-- src/Ucm.elm | 2 +- src/Workspace.elm | 29 +---- src/css/main.css | 197 ++++++++++++++++++++++++++-- src/hub.js | 18 +-- src/init.js | 11 ++ src/system.js | 29 +++++ src/ucm.js | 18 +-- 16 files changed, 696 insertions(+), 284 deletions(-) create mode 100644 src/System.elm create mode 100644 src/init.js create mode 100644 src/system.js diff --git a/package.json b/package.json index cab57a9..08fd044 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "npm run makeElmEnv; webpack --mode production --config webpack.prod.js", "clean": "rm -rf dist", "watch": "npm run makeElmEnv; webpack --mode development --watch --config webpack.dev.js", - "start": "npm run makeElmEnv; webpack serve --mode development --port 1234 --config webpack.dev.js", + "start": "npm run makeElmEnv; webpack serve --mode development --port 1234 --config webpack.dev.js", "test": "elm-test", "makeElmEnv": "node ./scripts/makeElmEnv.mjs" }, diff --git a/src/App.elm b/src/App.elm index bc8fc43..70f377d 100644 --- a/src/App.elm +++ b/src/App.elm @@ -6,16 +6,19 @@ import CodebaseTree import Definition.Reference exposing (Reference(..)) import Finder import HashQualified exposing (HashQualified(..)) -import Html exposing (Html, a, aside, div, h1, header, nav, text) -import Html.Attributes exposing (href, id, target) +import Html exposing (Html, a, aside, div, h1, h3, header, nav, section, span, text) +import Html.Attributes exposing (class, href, id, target) +import Html.Events exposing (onClick) import KeyboardShortcut -import KeyboardShortcut.Key exposing (Key(..)) +import KeyboardShortcut.Key as Key exposing (Key(..)) import KeyboardShortcut.KeyboardEvent as KeyboardEvent exposing (KeyboardEvent) import RelativeTo exposing (RelativeTo(..)) import RemoteData exposing (RemoteData(..)) import Route exposing (Route) +import System exposing (OperatingSystem(..), System) import UI import UI.Icon as Icon +import UI.Modal as Modal import Url exposing (Url) import Workspace @@ -27,6 +30,7 @@ import Workspace type Modal = NoModal | FinderModal Finder.Model + | HelpModal type alias Model = @@ -37,11 +41,16 @@ type alias Model = , workspace : Workspace.Model , modal : Modal , keyboardShortcut : KeyboardShortcut.Model + , system : System } -init : () -> Url -> Nav.Key -> ( Model, Cmd Msg ) -init _ url navKey = +type alias Flags = + { system : { operatingSystem : String } } + + +init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) +init flags url navKey = let route = Route.fromUrl url @@ -62,6 +71,9 @@ init _ url navKey = relativeTo = Maybe.withDefault Codebase (Route.relativeTo route) + system = + System.fromRecord flags.system + model = { navKey = navKey , route = route @@ -69,7 +81,8 @@ init _ url navKey = , workspace = workspace , codebaseTree = codebaseTree , modal = NoModal - , keyboardShortcut = KeyboardShortcut.init + , keyboardShortcut = KeyboardShortcut.init system.operatingSystem + , system = system } in ( model @@ -86,6 +99,8 @@ type Msg | UrlChanged Url | Keydown KeyboardEvent | OpenDefinition Reference + | ShowHelpModal + | CloseModal -- sub msgs | FinderMsg Finder.Msg | WorkspaceMsg Workspace.Msg @@ -111,6 +126,13 @@ update msg model = OpenDefinition ref -> openDefinition model ref + ShowHelpModal -> + ( { model | modal = HelpModal }, Cmd.none ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none ) + + -- Sub msgs WorkspaceMsg wMsg -> let ( workspace, wCmd, outMsg ) = @@ -144,9 +166,6 @@ update msg model = FinderMsg fMsg -> case model.modal of - NoModal -> - ( model, Cmd.none ) - FinderModal fModel -> let ( fm, fc, out ) = @@ -162,6 +181,9 @@ update msg model = Finder.OpenDefinition ref -> openDefinition { model | modal = NoModal } ref + _ -> + ( model, Cmd.none ) + KeyboardShortcutMsg kMsg -> let ( keyboardShortcut, cmd ) = @@ -210,23 +232,45 @@ keydown model keyboardEvent = let shortcut = KeyboardShortcut.fromKeyboardEvent model.keyboardShortcut keyboardEvent + + noOp = + ( model, Cmd.none ) in case shortcut of KeyboardShortcut.Chord Ctrl (K _) -> showFinder model KeyboardShortcut.Chord Meta (K _) -> + if model.system.operatingSystem == System.MacOS then + showFinder model + + else + noOp + + KeyboardShortcut.Sequence _ ForwardSlash -> showFinder model + KeyboardShortcut.Chord Shift QuestionMark -> + ( { model | modal = HelpModal }, Cmd.none ) + + KeyboardShortcut.Sequence _ Escape -> + if model.modal == HelpModal then + ( { model | modal = NoModal }, Cmd.none ) + + else + noOp + _ -> - ( model, Cmd.none ) + noOp -showFinder : { m | modal : Modal } -> ( { m | modal : Modal }, Cmd Msg ) +showFinder : + { m | system : System, modal : Modal } + -> ( { m | system : System, modal : Modal }, Cmd Msg ) showFinder model = let ( fm, fcmd ) = - Finder.init + Finder.init model.system in ( { model | modal = FinderModal fm }, Cmd.map FinderMsg fcmd ) @@ -252,21 +296,83 @@ viewMainSidebar model = , nav [] [ a [ href "https://unison-lang.org", target "_blank" ] [ Icon.view Icon.UnisonMark ] , a [ href "https://unison-lang.org/docs", target "_blank" ] [ text "Docs" ] - , a [ href "https://unison-lang.org/community", target "_blank" ] [ text "Community" ] , a [ href "https://unison-lang.org/docs/language-reference", target "_blank" ] [ text "Language Reference" ] + , a [ href "https://unison-lang.org/community", target "_blank" ] [ text "Community" ] + , a [ class "show-help", onClick ShowHelpModal ] [ text "Keyboard Shortcuts", KeyboardShortcut.view model.keyboardShortcut (KeyboardShortcut.single QuestionMark) ] ] ] -viewModal : Modal -> Html Msg -viewModal modal = - case modal of +viewHelpModal : OperatingSystem -> KeyboardShortcut.Model -> Html Msg +viewHelpModal os keyboardShortcut = + let + viewRow label instructions = + div + [ class "row" ] + [ label + , div [ class "instructions" ] instructions + ] + + viewInstructions label shortcuts = + viewRow label [ KeyboardShortcut.viewShortcuts keyboardShortcut shortcuts ] + + openFinderInstructions = + case os of + MacOS -> + [ KeyboardShortcut.Chord Meta (K Key.Lower), KeyboardShortcut.Chord Ctrl (K Key.Lower), KeyboardShortcut.single ForwardSlash ] + + _ -> + [ KeyboardShortcut.Chord Ctrl (K Key.Lower), KeyboardShortcut.single ForwardSlash ] + + content = + Modal.Content + (section + [ class "shortcuts" ] + [ div [ class "shortcut-group" ] + [ h3 [] [ text "General" ] + , viewInstructions (span [] [ text "Keyboard shortcuts", UI.subtle " (this dialog)" ]) [ KeyboardShortcut.single QuestionMark ] + , viewInstructions (text "Open Finder") openFinderInstructions + , viewInstructions (text "Move focus up") [ KeyboardShortcut.single ArrowUp, KeyboardShortcut.single (K Key.Lower) ] + , viewInstructions (text "Move focus down") [ KeyboardShortcut.single ArrowDown, KeyboardShortcut.single (J Key.Lower) ] + , viewInstructions (text "Close focused definition") [ KeyboardShortcut.single (X Key.Lower) ] + ] + , div [ class "shortcut-group" ] + [ h3 [] [ text "Finder" ] + , viewInstructions (text "Clear search query") [ KeyboardShortcut.single Escape ] + , viewInstructions (span [] [ text "Close", UI.subtle " (when search query is empty)" ]) [ KeyboardShortcut.single Escape ] + , viewInstructions (text "Move focus up") [ KeyboardShortcut.single ArrowUp ] + , viewInstructions (text "Move focus down") [ KeyboardShortcut.single ArrowDown ] + , viewInstructions (text "Open focused definition") [ KeyboardShortcut.single Enter ] + , viewRow (text "Open definition") + [ KeyboardShortcut.viewBase + [ KeyboardShortcut.viewKey os Semicolon False + , KeyboardShortcut.viewThen + , KeyboardShortcut.viewKeyBase "1-9" False + ] + ] + ] + ] + ) + in + Modal.modal "help-modal" CloseModal content + |> Modal.withHeader "Keyboard shortcuts" + |> Modal.view + + +viewModal : + { m | system : System, modal : Modal, keyboardShortcut : KeyboardShortcut.Model } + -> Html Msg +viewModal model = + case model.modal of NoModal -> UI.nothing FinderModal m -> Html.map FinderMsg (Finder.view m) + HelpModal -> + viewHelpModal model.system.operatingSystem model.keyboardShortcut + view : Model -> Browser.Document Msg view model = @@ -275,7 +381,7 @@ view model = [ div [ id "app" ] [ viewMainSidebar model , Html.map WorkspaceMsg (Workspace.view model.workspace) - , viewModal model.modal + , viewModal model ] ] } diff --git a/src/Finder.elm b/src/Finder.elm index 4a9f338..b02e34f 100644 --- a/src/Finder.elm +++ b/src/Finder.elm @@ -48,6 +48,7 @@ import List.Nonempty as NEL import RemoteData exposing (RemoteData(..), WebData) import SearchResults exposing (SearchResults(..)) import Syntax +import System exposing (System) import Task import UI import UI.Icon as Icon @@ -69,11 +70,11 @@ type alias Model = } -init : ( Model, Cmd Msg ) -init = +init : System -> ( Model, Cmd Msg ) +init system = ( { query = "" , results = NotAsked - , keyboardShortcut = KeyboardShortcut.init + , keyboardShortcut = KeyboardShortcut.init system.operatingSystem } , focusSearchInput ) @@ -320,7 +321,7 @@ viewMatch keyboardShortcut match isFocused shortcut = let shortcutIndicator = if isFocused then - KeyboardShortcut.viewShortcut keyboardShortcut (Sequence Nothing Key.Enter) + KeyboardShortcut.view keyboardShortcut (Sequence Nothing Key.Enter) else case shortcut of @@ -328,7 +329,7 @@ viewMatch keyboardShortcut match isFocused shortcut = UI.nothing Just key -> - KeyboardShortcut.viewShortcut keyboardShortcut (Sequence (Just Key.Semicolon) key) + KeyboardShortcut.view keyboardShortcut (Sequence (Just Key.Semicolon) key) viewMatch_ reference icon naming source = tr @@ -338,7 +339,7 @@ viewMatch keyboardShortcut match isFocused shortcut = [ td [ class "category" ] [ Icon.view icon ] , naming , td [ class "source" ] [ source ] - , td [ class "shortcut" ] [ shortcutIndicator ] + , td [] [ div [ class "shortcut" ] [ shortcutIndicator ] ] ] in case match.item of @@ -401,26 +402,32 @@ view model = _ -> UI.nothing + + content = + Modal.CustomContent + (div + [] + [ header [] + [ Icon.view Icon.Search + , input + [ type_ "text" + , id "search" + , autocomplete False + , spellcheck False + , placeholder "Search by name, namespace, and/or type" + , onInput UpdateQuery + , value model.query + ] + [] + , a [ class "reset", onClick ResetOrClose ] [ Icon.view Icon.X ] + ] + , results + ] + ) in - -- We stopPropagation such that movement shortcuts, like J or K, for the - -- workspace aren't triggered when in the modal when the use is trying to - -- type those letters into the search field - Modal.view - Close - [ id "finder", KeyboardEvent.stopPropagationOn KeyboardEvent.Keydown Keydown ] - [ header [] - [ Icon.view Icon.Search - , input - [ type_ "text" - , id "search" - , autocomplete False - , spellcheck False - , placeholder "Search by name, namespace, and/or type" - , onInput UpdateQuery - , value model.query - ] - [] - , a [ class "reset", onClick ResetOrClose ] [ Icon.view Icon.X ] - ] - , results - ] + Modal.modal "finder" Close content + -- We stopPropagation such that movement shortcuts, like J or K, for the + -- workspace aren't triggered when in the modal when the use is trying to + -- type those letters into the search field + |> Modal.withAttributes [ KeyboardEvent.stopPropagationOn KeyboardEvent.Keydown Keydown ] + |> Modal.view diff --git a/src/Hub.elm b/src/Hub.elm index 0b070e1..8ea7c5b 100644 --- a/src/Hub.elm +++ b/src/Hub.elm @@ -4,7 +4,7 @@ import App import Browser -main : Program () App.Model App.Msg +main : Program App.Flags App.Model App.Msg main = Browser.application { init = App.init diff --git a/src/KeyboardShortcut.elm b/src/KeyboardShortcut.elm index ab8f605..d5d0ad6 100644 --- a/src/KeyboardShortcut.elm +++ b/src/KeyboardShortcut.elm @@ -43,6 +43,7 @@ import KeyboardShortcut.Key as Key exposing (Key) import KeyboardShortcut.KeyboardEvent as KeyboardEvent exposing (KeyboardEvent) import List.Nonempty as NEL import Process +import System exposing (OperatingSystem) import Task @@ -58,16 +59,25 @@ type +-- CREATE + + +single : Key -> KeyboardShortcut +single key = + Sequence Nothing key + + + -- MODEL type alias Model = - Maybe Key + { key : Maybe Key, operatingSystem : OperatingSystem } -init : Model -init = - Nothing +init : OperatingSystem -> Model +init os = + { key = Nothing, operatingSystem = os } @@ -83,19 +93,19 @@ update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of CollectKey key -> - ( Just key, decay key ) + ( { model | key = Just key }, decay key ) Decay key -> - case model of + case model.key of Just k -> if k == key then - ( Nothing, Cmd.none ) + ( { model | key = Nothing }, Cmd.none ) else ( model, Cmd.none ) Nothing -> - ( Nothing, Cmd.none ) + ( { model | key = Nothing }, Cmd.none ) collect : Model -> Key -> ( Model, Cmd Msg ) @@ -115,7 +125,7 @@ decay key = fromKeyboardEvent : Model -> KeyboardEvent -> KeyboardShortcut fromKeyboardEvent model event = if Key.isModifier event.key then - Sequence model event.key + Sequence model.key event.key else case KeyboardEvent.modifiersHeld event of @@ -123,44 +133,72 @@ fromKeyboardEvent model event = Chord (NEL.head modifiers) event.key Nothing -> - Sequence model event.key + Sequence model.key event.key startedSequenceWith : Model -> Key -> Bool startedSequenceWith model key = - Just key == model + Just key == model.key -- VIEW -viewKey : Key -> Bool -> Html msg -viewKey key isActive = - span [ classList [ ( "key", True ), ( "active", isActive ) ] ] [ text (Key.view key) ] +viewKeyBase : String -> Bool -> Html msg +viewKeyBase key isActive = + span [ classList [ ( "key", True ), ( "active", isActive ) ] ] [ text key ] + + +viewBase : List (Html msg) -> Html msg +viewBase shortcut = + span [ class "keyboard-shortcut" ] shortcut + +viewKey : OperatingSystem -> Key -> Bool -> Html msg +viewKey os key isActive = + viewKeyBase (Key.view os key) isActive -viewShortcut : Model -> KeyboardShortcut -> Html msg -viewShortcut model shortcut = + +viewThen : Html msg +viewThen = + span [ class "then" ] [ text "then" ] + + +view : Model -> KeyboardShortcut -> Html msg +view model shortcut = let - instruction text_ = - span [ class "instruction" ] [ text text_ ] + os = + model.operatingSystem content = case shortcut of Sequence Nothing key -> - [ viewKey key False ] + [ viewKey os key False ] Sequence (Just keyA) keyB -> - [ viewKey keyA (startedSequenceWith model keyA) - , instruction "then" - , viewKey keyB False + [ viewKey os keyA (startedSequenceWith model keyA) + , viewThen + , viewKey os keyB False ] Chord mod key -> - [ viewKey mod False - , instruction "plus" - , viewKey key False + [ viewKey os mod False + , viewKey os key False ] in - span [ class "keyboard-shortcut" ] content + viewBase content + + +viewShortcuts : Model -> List KeyboardShortcut -> Html msg +viewShortcuts model shortcuts = + let + or = + span [ class "separator" ] [ text "or" ] + + instructions = + shortcuts + |> List.map (view model) + |> List.intersperse or + in + span [ class "keyboard-shortcuts" ] instructions diff --git a/src/KeyboardShortcut/Key.elm b/src/KeyboardShortcut/Key.elm index ead155e..8f24cff 100644 --- a/src/KeyboardShortcut/Key.elm +++ b/src/KeyboardShortcut/Key.elm @@ -1,3 +1,20 @@ +{-- + + KeyboardShortcut.Key + ==================== + + Parsing from KeyboardEvent.key string + ------------------------------------- + + Not all `key` values are consistent across browsers: + https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + + This library does not attempt to support older inconsistent browser values, + besides `Meta` and `OS` for Firefox. + +--} + + module KeyboardShortcut.Key exposing ( Key(..) , LetterCase(..) @@ -9,6 +26,7 @@ module KeyboardShortcut.Key exposing ) import Json.Decode as Decode +import System exposing (OperatingSystem(..)) type LetterCase @@ -91,6 +109,7 @@ type Key | Plus | Minus | ForwardSlash + | QuestionMark | Raw String @@ -359,10 +378,15 @@ fromString str = "Tab" -> Tab + -- Windows key is "OS" in Firefox, but will fixed to + -- produce Meta in: https://bugzilla.mozilla.org/show_bug.cgi?id=1232918 + "OS" -> + Meta + "Meta" -> Meta - "Space" -> + " " -> Space "Escape" -> @@ -467,6 +491,9 @@ fromString str = "/" -> ForwardSlash + "?" -> + QuestionMark + _ -> Raw str @@ -475,164 +502,95 @@ fromString str = -- VIEW -view : Key -> String -view key = - case key of - A Lower -> - "a" - - A Upper -> - "A" - - B Lower -> - "b" - - B Upper -> - "B" - - C Lower -> - "c" - - C Upper -> - "C" - - D Lower -> - "d" - - D Upper -> - "D" - - E Lower -> - "e" - - E Upper -> - "E" +view : OperatingSystem -> Key -> String +view os key = + let + letter l casing = + case casing of + Lower -> + String.toLower l - F Lower -> - "f" - - F Upper -> - "F" - - G Lower -> - "g" - - G Upper -> - "G" - - H Lower -> - "h" - - H Upper -> - "H" - - I Lower -> - "i" - - I Upper -> - "I" - - J Lower -> - "j" - - J Upper -> - "J" - - K Lower -> - "k" - - K Upper -> - "K" - - L Lower -> - "l" - - L Upper -> - "L" - - M Lower -> - "m" - - M Upper -> - "M" - - N Lower -> - "n" + Upper -> + String.toUpper l + in + case key of + A casing -> + letter "a" casing - N Upper -> - "N" + B casing -> + letter "b" casing - O Lower -> - "o" + C casing -> + letter "c" casing - O Upper -> - "O" + D casing -> + letter "d" casing - P Lower -> - "p" + E casing -> + letter "e" casing - P Upper -> - "P" + F casing -> + letter "f" casing - Q Lower -> - "q" + G casing -> + letter "g" casing - Q Upper -> - "Q" + H casing -> + letter "h" casing - R Lower -> - "r" + I casing -> + letter "i" casing - R Upper -> - "R" + J casing -> + letter "j" casing - S Lower -> - "s" + K casing -> + letter "k" casing - S Upper -> - "S" + L casing -> + letter "l" casing - T Lower -> - "t" + M casing -> + letter "m" casing - T Upper -> - "T" + N casing -> + letter "n" casing - U Lower -> - "u" + O casing -> + letter "o" casing - U Upper -> - "U" + P casing -> + letter "p" casing - V Lower -> - "v" + Q casing -> + letter "q" casing - V Upper -> - "V" + R casing -> + letter "r" casing - W Lower -> - "w" + S casing -> + letter "s" casing - W Upper -> - "W" + T casing -> + letter "t" casing - X Lower -> - "x" + U casing -> + letter "u" casing - X Upper -> - "X" + V casing -> + letter "v" casing - Y Lower -> - "y" + W casing -> + letter "w" casing - Y Upper -> - "Y" + X casing -> + letter "x" casing - Z Lower -> - "z" + Y casing -> + letter "y" casing - Z Upper -> - "Z" + Z casing -> + letter "z" casing Semicolon -> ";" @@ -659,18 +617,24 @@ view key = "⇧" Ctrl -> - "CTRL" + "Ctrl" Alt -> - "ALT" + "Alt" Tab -> "⇥ " - -- Windows -> "⊞" - -- Command -> "⌘ " Meta -> - "Meta" + case os of + Windows -> + "⊞" + + MacOS -> + "⌘ " + + _ -> + "Meta" Space -> "Space" @@ -777,5 +741,8 @@ view key = ForwardSlash -> "/" + QuestionMark -> + "?" + Raw str -> str diff --git a/src/System.elm b/src/System.elm new file mode 100644 index 0000000..8eb520c --- /dev/null +++ b/src/System.elm @@ -0,0 +1,44 @@ +-- This module pairs with src/system.js + + +module System exposing (..) + + +type OperatingSystem + = MacOS + | Windows + | Linux + | Android + | IOS + | Unknown + + +type alias System = + { operatingSystem : OperatingSystem } + + +fromRecord : { operatingSystem : String } -> System +fromRecord { operatingSystem } = + System (operatingSystemFromString operatingSystem) + + +operatingSystemFromString : String -> OperatingSystem +operatingSystemFromString rawOs = + case rawOs of + "macOS" -> + MacOS + + "iOS" -> + IOS + + "Windows" -> + Windows + + "Android" -> + Android + + "Linux" -> + Linux + + _ -> + Unknown diff --git a/src/UI.elm b/src/UI.elm index 06f7c8d..8d7d899 100644 --- a/src/UI.elm +++ b/src/UI.elm @@ -24,6 +24,11 @@ spinner = div [ class "spinner" ] [ text "Loading..." ] +subtle : String -> Html msg +subtle label = + span [ class "subtle" ] [ text label ] + + loadingPlaceholder : Html msg loadingPlaceholder = div [ class "loading-placeholder" ] [] diff --git a/src/UI/Modal.elm b/src/UI/Modal.elm index aaf5395..d905958 100644 --- a/src/UI/Modal.elm +++ b/src/UI/Modal.elm @@ -1,20 +1,89 @@ -module UI.Modal exposing (view) +module UI.Modal exposing + ( Content(..) + , Modal + , modal + , view + , withAttributes + , withHeader + ) -import Html exposing (Attribute, Html, div) +import Html exposing (Attribute, Html, a, div, h2, header, section, text) import Html.Attributes exposing (class, id, tabindex) -import Html.Events exposing (on) +import Html.Events exposing (on, onClick) import Json.Decode as Decode +import UI +import UI.Icon as Icon -view : msg -> List (Attribute msg) -> List (Html msg) -> Html msg -view closeMsg attrs content = - div [ id overlayId, on "click" (decodeOverlayClick closeMsg) ] - [ div (tabindex 0 :: class "modal" :: attrs) content - ] +type Content msg + = Content (Html msg) + | CustomContent (Html msg) + + +type alias Modal msg = + { id : String + , closeMsg : msg + , attributes : List (Attribute msg) + , header : Maybe (Html msg) + , content : Content msg + } + + +modal : String -> msg -> Content msg -> Modal msg +modal id closeMsg content = + { id = id + , closeMsg = closeMsg + , attributes = [] + , header = Nothing + , content = content + } + + +withHeader : String -> Modal msg -> Modal msg +withHeader title modal_ = + { modal_ | header = Just (text title) } + +withAttributes : List (Attribute msg) -> Modal msg -> Modal msg +withAttributes attrs modal_ = + { modal_ | attributes = modal_.attributes ++ attrs } --- INTERNAL +view : Modal msg -> Html msg +view modal_ = + let + header_ = + modal_.header + |> Maybe.map + (\title -> + header [ class "modal-header " ] + [ h2 [] [ title ] + , a [ class "close-modal", onClick modal_.closeMsg ] + [ Icon.view Icon.X ] + ] + ) + |> Maybe.withDefault UI.nothing + + content = + case modal_.content of + Content c -> + section [ class "modal-content" ] [ c ] + + CustomContent c -> + c + in + view_ modal_.closeMsg (id modal_.id :: modal_.attributes) [ header_, content ] + + + +-- INTERNALS + + +view_ : msg -> List (Attribute msg) -> List (Html msg) -> Html msg +view_ closeMsg attrs content = + div [ id overlayId, on "click" (decodeOverlayClick closeMsg) ] + [ div (tabindex 0 :: class "modal" :: attrs) content + ] overlayId : String diff --git a/src/Ucm.elm b/src/Ucm.elm index a700a19..d00e583 100644 --- a/src/Ucm.elm +++ b/src/Ucm.elm @@ -4,7 +4,7 @@ import App import Browser -main : Program () App.Model App.Msg +main : Program App.Flags App.Model App.Msg main = Browser.application { init = App.init diff --git a/src/Workspace.elm b/src/Workspace.elm index 4874e30..0371d77 100644 --- a/src/Workspace.elm +++ b/src/Workspace.elm @@ -182,38 +182,19 @@ keydown model keyboardEvent = WorkspaceItems.prev model in ( prev, scrollToCmd prev, openDefinitionsFocusToOutMsg prev ) - - passthrough = - ( model, Cmd.none, None ) in case keyboardEvent.key of ArrowDown -> - if keyboardEvent.shiftKey then - nextDefinition - - else - passthrough + nextDefinition J _ -> - if keyboardEvent.shiftKey then - nextDefinition - - else - passthrough + nextDefinition ArrowUp -> - if keyboardEvent.shiftKey then - prevDefinitions - - else - passthrough + prevDefinitions K _ -> - if keyboardEvent.shiftKey then - prevDefinitions - - else - passthrough + prevDefinitions X _ -> let @@ -226,7 +207,7 @@ keydown model keyboardEvent = ( without, Cmd.none, openDefinitionsFocusToOutMsg without ) _ -> - passthrough + ( model, Cmd.none, None ) diff --git a/src/css/main.css b/src/css/main.css index 387e52b..9ed3f24 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -42,6 +42,7 @@ table { --font-monospace: "Roboto Mono var", monospace; --font-size-base: 1rem; + --font-size-large: 1.5rem; --font-size-medium: 0.875rem; --font-size-small: 0.75rem; @@ -91,6 +92,11 @@ table { --color-main-mark-fg: var(--color-blue-base); --color-main-mark-bg: transparent; + --color-keyboard-shortcut-key-fg: var(--color-gray-base); + --color-keyboard-shortcut-key-bg: var(--color-gray-lighten-50); + --color-keyboard-shortcut-then: var(--color-gray-lighten-20); + --color-keyboard-shortcut-separator: var(--color-gray-lighten-30); + --color-sidebar-fg: var(--color-gray-lighten-50); --color-sidebar-bg: var(--color-gray-darken-20); --color-sidebar-context-fg: var(--color-brand-orange); @@ -99,6 +105,10 @@ table { --color-sidebar-subtle-bg: transparent; --color-sidebar-focus-fg: var(--color-gray-lighten-60); --color-sidebar-focus-bg: var(--color-gray-darken-5); + --color-sidebar-keyboard-shortcut-key-fg: var(--color-gray-lighten-30); + --color-sidebar-keyboard-shortcut-key-bg: var(--color-gray-base); + --color-sidebar-keyboard-shortcut-then: var(--color-gray-lighten-20); + --color-sidebar-keyboard-shortcut-separator: var(--color-gray-base); --color-workspace-fg: var(--color-main-fg); --color-workspace-bg: var(--color-main-bg); @@ -134,8 +144,10 @@ table { --color-modal-focus-bg: var(--color-gray-lighten-55); --color-modal-focus-subtle-fg: var(--color-gray-base); --color-modal-focus-subtle-bg: var(--color-gray-lighten-50); + --color-modal-title-fg: var(--color-gray-lighten-20); + --color-modal-title-bg: transparent; --color-modal-mark-fg: var(--color-blue-base); - --colodalmain-mark-bg: transparent; + --color-modal-mark-bg: transparent; /* Icons */ --color-icon-type: var(--color-brand-orange); @@ -253,6 +265,18 @@ input { color: var(--color-main-subtle-fg); } +h1 { + font-size: var(--font-size-large); +} + +h2 { + font-size: var(--font-size-base); +} + +h3 { + font-size: var(--font-size-medium); +} + /* -- Syntax --------------------------------------------------------------- */ pre { @@ -432,6 +456,10 @@ code a:active { color: var(--color-brand-bright-red); } +.subtle { + color: var(--color-main-subtle-fg); +} + /* Loading placeholders */ .loading-placeholder { @@ -461,14 +489,54 @@ code a:active { position: relative; background: var(--color-modal-bg); border-radius: var(--border-radius-base); - width: 50rem; + width: auto; margin-top: 4rem; overflow: hidden; + height: -moz-fit-content; height: fit-content; animation: slide-up 0.2s var(--anim-elastic); box-shadow: 0 0.375rem 1rem var(--color-modal-shadow), 0 0 0 1px var(--color-modal-border); z-index: (--layer-modal); + font-size: var(--font-size-medium); +} + +.modal-header { + padding: 1.5rem; + display: flex; + flex-direction: row; + color: var(--color-modal-title-bg); +} + +.modal-header + .modal-content { + padding-top: 0.5rem; +} + +.modal-header h2 { + font-size: var(--font-size-base); + height: 1.5rem; + color: var(--color-modal-title-fg); +} + +.modal-header .close-modal { + width: 1.5rem; + height: 1.5rem; + font-size: var(--font-size-medium); + justify-self: right; + margin-top: -0.5rem; + margin-right: -1rem; + margin-left: auto; +} + +.modal-header .close-modal .icon { + color: var(--color-modal-subtle-fg); +} +.modal-header .close-modal:hover .icon { + color: var(--color-modal-fg); +} + +.modal-content { + padding: 1.5rem; } .modal:focus { @@ -608,26 +676,52 @@ button.secondary:hover { /* Keyboard shortcuts */ +.keyboard-shortcuts { + display: flex; + flex-direction: row; + justify-self: flex-end; + margin-left: auto; +} + +.keyboard-shortcuts .separator { + display: inline-flex; + height: 1.5rem; + font-size: 0.875rem; + color: var(--color-keyboard-shortcut-separator); + align-items: center; + margin: 0 0.4rem; + line-height: 1; +} + +.keyboard-shortcut { + display: flex; + flex-direction: row; + gap: 0.25rem; +} + .keyboard-shortcut .key { height: 1.5rem; - width: 1.5rem; + min-width: 1.5rem; font-size: 0.75rem; + padding: 0.375rem; border-radius: var(--border-radius-base); text-align: center; display: inline-flex; justify-content: center; align-content: center; align-items: center; - background: var(--color-main-subtle-bg); - color: var(--color-main-subtle-fg); + color: var(--color-keyboard-shortcut-key-fg); + background: var(--color-keyboard-shortcut-key-bg); } -.keyboard-shortcut .instruction { +.keyboard-shortcut .then { display: inline-flex; - width: 2rem; + height: 1.5rem; font-size: 0.625rem; - justify-content: center; - color: var(--color-main-subtle-fg); + line-heigth: 1; + margin: 0 0.1rem; + color: var(--color-keyboard-shortcut-then); + align-items: center; } /* -- Application ---------------------------------------------------------- */ @@ -696,7 +790,7 @@ button.secondary:hover { display: flex; user-select: none; align-items: center; - border-radius: 4px; + border-radius: var(--border-radius-base); padding-left: 0.5rem; margin-bottom: 0.125rem; height: 1.75rem; @@ -753,6 +847,8 @@ button.secondary:hover { animation: slide-down 0.2s ease-out; } +/* -- Main Sidebar Nav ----------------------------------------------------- */ + #main-sidebar nav { display: flex; flex-direction: column; @@ -763,7 +859,12 @@ button.secondary:hover { #main-sidebar nav a { height: 1.5rem; + display: flex; + align-items: center; transition: all 0.2s; + padding-left: 0.5rem; + margin-left: -0.5rem; + border-radius: var(--border-radius-base); } #main-sidebar nav a, @@ -777,6 +878,33 @@ button.secondary:hover { text-decoration: none; } +#main-sidebar nav .show-help { + margin-top: 2rem; + padding-right: 0.25rem; + line-height: 1; + display: flex; + height: 2rem; + align-items: center; + font-weight: bold; + color: var(--color-sidebar-fg); + cursor: help; +} + +#main-sidebar nav .show-help .keyboard-shortcut { + justify-self: flex-end; + margin-left: auto; +} + +#main-sidebar .keyboard-shortcut .key { + color: var(--color-sidebar-keyboard-shortcut-key-fg); + background: var(--color-sidebar-keyboard-shortcut-key-bg); + font-weight: normal; +} + +#main-sidebar nav .show-help:hover { + background: var(--color-sidebar-focus-bg); +} + /* -- Workspace ------------------------------------------------------------ */ #workspace { @@ -1005,8 +1133,50 @@ button.secondary:hover { color: var(--color-workspace-item-focus-fg); } +/* -- HelpModal ------------------------------------------------------------ */ + +#help-modal .shortcuts { + display: flex; + flex-direction: row; + gap: 2.625rem; +} + +#help-modal .shortcuts .shortcut-group { + width: 20rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +#help-modal .shortcuts h3 { + height: 1.5rem; + display: flex; + align-items: center; +} + +#help-modal .shortcuts .row { + display: flex; + height: 1.5rem; + align-items: center; +} + +#help-modal .shortcuts .instructions { + display: flex; + flex-direction: row; + justify-self: flex-end; + margin-left: auto; +} + +#help-modal .subtle { + color: var(--color-modal-subtle-fg); +} + /* -- Finder --------------------------------------------------------------- */ +#finder { + width: 50rem; +} + #finder:focus { outline: none; } @@ -1167,8 +1337,9 @@ button.secondary:hover { #finder .definition-match .shortcut { white-space: nowrap; - text-align: right; - padding-top: 1.125rem; + display: flex; + align-items: center; + height: 100%; } #finder .definition-match .keyboard-shortcut { @@ -1187,7 +1358,7 @@ button.secondary:hover { background: var(--color-modal-focus-subtle-bg); } -#finder .definition-match .keyboard-shortcut .instruction { +#finder .definition-match .keyboard-shortcut .then { color: var(--color-modal-subtle-fg); } diff --git a/src/hub.js b/src/hub.js index 76fac31..a46a9f4 100644 --- a/src/hub.js +++ b/src/hub.js @@ -1,16 +1,8 @@ -import "./css/fonts.css"; -import "./css/main.css"; +import "./init"; +import system from "./system"; import { Elm } from "./Hub.elm"; -// The main entry point for the `hub` target of the Codebase UI. - -console.log(` - _____ _ -| | |___|_|___ ___ ___ -| | | | |_ -| . | | -|_____|_|_|_|___|___|_|_| - +const flags = { system: system() }; -`); - -Elm.Hub.init(); +// The main entry point for the `hub` target of the Codebase UI. +Elm.Hub.init({ flags }); diff --git a/src/init.js b/src/init.js new file mode 100644 index 0000000..46b2178 --- /dev/null +++ b/src/init.js @@ -0,0 +1,11 @@ +import "./css/fonts.css"; +import "./css/main.css"; + +console.log(` + _____ _ +| | |___|_|___ ___ ___ +| | | | |_ -| . | | +|_____|_|_|_|___|___|_|_| + + +`); diff --git a/src/system.js b/src/system.js new file mode 100644 index 0000000..d988dee --- /dev/null +++ b/src/system.js @@ -0,0 +1,29 @@ +const macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"]; +const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"]; +const iosPlatforms = ["iPhone", "iPad", "iPod"]; + +function detectOs(nav) { + const { userAgent, platform } = nav; + + if (macosPlatforms.includes(platform)) { + return "macOS"; + } else if (iosPlatforms.includes(platform)) { + return "iOS"; + } else if (windowsPlatforms.includes(platform)) { + return "Windows"; + } else if (/Android/.test(userAgent)) { + return "Android"; + } else if (/Linux/.test(platform)) { + return "Linux"; + } else { + return "Unknown"; + } +} + +function platform() { + return { + operatingSystem: detectOs(window.navigator), + }; +} + +export default platform; diff --git a/src/ucm.js b/src/ucm.js index 64a523f..2be4287 100644 --- a/src/ucm.js +++ b/src/ucm.js @@ -1,16 +1,8 @@ -import "./css/fonts.css"; -import "./css/main.css"; +import "./init"; +import system from "./system"; import { Elm } from "./Ucm.elm"; -// The main entry point for the `ucm` target of the Codebase UI. - -console.log(` - _____ _ -| | |___|_|___ ___ ___ -| | | | |_ -| . | | -|_____|_|_|_|___|___|_|_| - +const flags = { system: system() }; -`); - -Elm.Ucm.init(); +// The main entry point for the `ucm` target of the Codebase UI. +Elm.Ucm.init({ flags });