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 });