diff --git a/src/App.elm b/src/App.elm index d0778ab..58e29ad 100644 --- a/src/App.elm +++ b/src/App.elm @@ -9,7 +9,7 @@ import Env exposing (AppContext(..), Env, OperatingSystem(..)) import Finder import Finder.SearchOptions as SearchOptions import FullyQualifiedName as FQN exposing (FQN) -import Html exposing (Html, a, div, h1, h2, h3, header, nav, p, section, span, strong, text) +import Html exposing (Html, a, div, h1, h2, h3, nav, p, section, span, strong, text) import Html.Attributes exposing (class, classList, href, id, rel, target, title) import Html.Events exposing (onClick) import Http @@ -26,6 +26,7 @@ import UI.AppHeader as AppHeader import UI.Banner as Banner import UI.Button as Button import UI.Click as Click exposing (Click(..)) +import UI.CopyField as CopyField import UI.Icon as Icon import UI.Modal as Modal import UI.Sidebar as Sidebar @@ -46,6 +47,7 @@ type Modal | HelpModal | ReportBugModal | PublishModal + | DownloadModal FQN type alias Model = @@ -498,8 +500,8 @@ viewAppHeader model = } -viewPerspective : Env -> Html Msg -viewPerspective env = +viewSidebarHeader : Env -> Html Msg +viewSidebarHeader env = case env.perspective of Codebase _ -> UI.nothing @@ -512,11 +514,27 @@ viewPerspective env = -- thats quite involved... isOverflowing = fqn |> FQN.toString |> String.length |> (\l -> l > 20) + + download = + case env.appContext of + UnisonShare -> + Button.iconThenLabel (ShowModal (DownloadModal fqn)) Icon.download "Download latest version" + |> Button.small + |> Button.view + |> List.singleton + |> Sidebar.headerItem [] + + Ucm -> + UI.nothing in - header - [ classList [ ( "perspective", True ), ( "is-overflowing", isOverflowing ) ] ] - [ UI.namespaceSlug - , h2 [ class "namespace" ] [ FQN.view fqn ] + Sidebar.header + [ Sidebar.headerItem + [ classList [ ( "is-overflowing", isOverflowing ) ] ] + [ UI.namespaceSlug + , h2 [ class "namespace" ] [ FQN.view fqn ] + ] + , download + , UI.divider ] @@ -587,7 +605,7 @@ viewMainSidebar model = Sidebar.view [ viewMainSidebarCollapseButton model , div [ class "expanded-content" ] - [ viewPerspective model.env + [ viewSidebarHeader model.env , div [ class "sidebar-scroll-area" ] [ sidebarContent , Sidebar.section @@ -621,6 +639,33 @@ viewMainSidebar model = ] +viewDownloadModal : FQN -> Html Msg +viewDownloadModal fqn = + let + prettyName = + FQN.toString fqn + + unqualified = + FQN.unqualifiedName fqn + + pullCommand = + "pull git@github.com:unisonweb/share.git:." ++ prettyName ++ " ." ++ unqualified + + content = + Modal.Content + (section + [] + [ p [] [ text "Download ", UI.bold prettyName, text " by pulling the namespace from Unison Share into a namespace in your local codebase:" ] + , CopyField.copyField (\_ -> CloseModal) pullCommand |> CopyField.withPrefix ".>" |> CopyField.view + , div [ class "hint" ] [ text "Copy and paste this command into UCM." ] + ] + ) + in + Modal.modal "download-modal" CloseModal content + |> Modal.withHeader ("Download " ++ prettyName) + |> Modal.view + + viewHelpModal : OperatingSystem -> KeyboardShortcut.Model -> Html Msg viewHelpModal os keyboardShortcut = let @@ -770,6 +815,9 @@ viewModal model = ReportBugModal -> viewReportBugModal model.env.appContext + DownloadModal fqn -> + viewDownloadModal fqn + viewAppLoading : AppContext -> Html msg viewAppLoading appContext = diff --git a/src/UI.elm b/src/UI.elm index 2cba2b1..f726681 100644 --- a/src/UI.elm +++ b/src/UI.elm @@ -1,6 +1,6 @@ module UI exposing (..) -import Html exposing (Attribute, Html, code, div, hr, pre, span, text) +import Html exposing (Attribute, Html, code, div, hr, pre, span, strong, text) import Html.Attributes exposing (class) import Html.Events exposing (onClick) import UI.Icon as Icon @@ -11,6 +11,11 @@ codeBlock attrs code_ = pre attrs [ code [] [ code_ ] ] +bold : String -> Html msg +bold text_ = + strong [] [ text text_ ] + + inlineCode : List (Attribute msg) -> Html msg -> Html msg inlineCode attrs code_ = code (class "inline-code" :: attrs) [ code_ ] @@ -60,7 +65,7 @@ emptyStateMessage message = divider : Html msg divider = - hr [] [] + hr [ class "divider" ] [] charWidth : Int -> String diff --git a/src/UI/CopyField.elm b/src/UI/CopyField.elm new file mode 100644 index 0000000..ce78ff4 --- /dev/null +++ b/src/UI/CopyField.elm @@ -0,0 +1,69 @@ +module UI.CopyField exposing (..) + +import Html exposing (Html, button, div, input, node, text) +import Html.Attributes exposing (attribute, class, readonly, type_, value) +import UI +import UI.Icon as Icon + + +type alias CopyField msg = + { prefix : Maybe String + , toCopy : String + , onCopy : String -> msg + } + + +copyField : (String -> msg) -> String -> CopyField msg +copyField onCopy toCopy = + { prefix = Nothing, toCopy = toCopy, onCopy = onCopy } + + +withPrefix : String -> CopyField msg -> CopyField msg +withPrefix prefix field = + { field | prefix = Just prefix } + + +withToCopy : String -> CopyField msg -> CopyField msg +withToCopy toCopy field = + { field | toCopy = toCopy } + + +view : CopyField msg -> Html msg +view field = + let + prefix = + field.prefix + |> Maybe.map (\p -> div [ class "copy-field-prefix" ] [ text p ]) + |> Maybe.withDefault UI.nothing + in + div [ class "copy-field" ] + [ div [ class "copy-field-field" ] + [ prefix + , div + [ class "copy-field-input" ] + [ input + [ type_ "text" + , class "copy-field-to-copy" + , value field.toCopy + , readonly True + ] + [] + ] + ] + , copyButton field.toCopy + ] + + + +-- HELPERS -------------------------------------------------------------------- + + +{-| We're not using UI.Button here since a click handler is added from +the webcomponent in JS land. +-} +copyButton : String -> Html msg +copyButton toCopy = + node "copy-on-click" + [ attribute "text" toCopy ] + [ button [ class "button contained default" ] [ Icon.view Icon.clipboard ] + ] diff --git a/src/UI/CopyOnClick.js b/src/UI/CopyOnClick.js new file mode 100644 index 0000000..0420b8d --- /dev/null +++ b/src/UI/CopyOnClick.js @@ -0,0 +1,27 @@ +// +// clickable content +// +// +// Use from Elm with an Icon: +// node "copy-on-click" [ ] [ UI.Icon.view UI.Icon.clipboard ] +class CopyOnClick extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.addEventListener("click", () => { + const text = this.getAttribute("text"); + + // writeText returns a promise with success/failure that we should + // probably do something with... + navigator.clipboard.writeText(text); + }); + } + + static get observedAttributes() { + return ["text"]; + } +} + +customElements.define("copy-on-click", CopyOnClick); diff --git a/src/UI/Icon.elm b/src/UI/Icon.elm index 75d27e0..e381d63 100644 --- a/src/UI/Icon.elm +++ b/src/UI/Icon.elm @@ -370,3 +370,12 @@ tagsOutlined = , path [ fill "currentColor", fillRule "evenodd", d "M8.62836 2.16552C8.81309 1.96026 9.12923 1.94362 9.33449 2.12835L13.6332 5.99715C14.2238 6.52872 14.2974 7.42865 13.801 8.04914L10.3904 12.3123C10.2179 12.528 9.90329 12.5629 9.68766 12.3904C9.47203 12.2179 9.43707 11.9033 9.60957 11.6877L13.0201 7.42444C13.1856 7.21761 13.1611 6.91763 12.9642 6.74045L8.66552 2.87165C8.46027 2.68692 8.44363 2.37077 8.62836 2.16552Z" ] [] , circle [ cx "4", cy "5", r "1.5", stroke "currentColor", fill "transparent" ] [] ] + + +clipboard : Icon msg +clipboard = + Icon "clipboard" + [] + [ path [ fill "currentColor", fillRule "evenodd", d "M8 2.25C8 2.11193 7.88807 2 7.75 2H6.25C6.11193 2 6 2.11193 6 2.25V2.75C6 2.88807 6.11193 3 6.25 3H7.75C7.88807 3 8 2.88807 8 2.75V2.25ZM6 1C5.44772 1 5 1.44772 5 2V3C5 3.55228 5.44772 4 6 4H8C8.55228 4 9 3.55228 9 3V2C9 1.44772 8.55228 1 8 1H6Z" ] [] + , path [ fill "currentColor", fillRule "evenodd", d "M3 2.5C3 2.22386 3.22386 2 3.5 2C3.77614 2 4 2.22386 4 2.5V10.5C4 10.7761 4.22386 11 4.5 11H9.5C9.77614 11 10 10.7761 10 10.5V2.5C10 2.22386 10.2239 2 10.5 2C10.7761 2 11 2.22386 11 2.5V11C11 11.5523 10.5523 12 10 12H4C3.44772 12 3 11.5523 3 11V2.5Z" ] [] + ] diff --git a/src/UI/Sidebar.elm b/src/UI/Sidebar.elm index e33ab18..17232bf 100644 --- a/src/UI/Sidebar.elm +++ b/src/UI/Sidebar.elm @@ -1,19 +1,29 @@ module UI.Sidebar exposing (..) -import Html exposing (Html, a, aside, h3, label, text) +import Html exposing (Attribute, Html, a, aside, div, h3, label, text) import Html.Attributes exposing (class, id) import Html.Events exposing (onClick) +header : List (Html msg) -> Html msg +header content = + Html.header [ class "sidebar-header" ] content + + +headerItem : List (Attribute msg) -> List (Html msg) -> Html msg +headerItem attrs content = + div (attrs ++ [ class "sidebar-header-item" ]) content + + section : String -> List (Html msg) -> Html msg section label content = Html.section [ class "sidebar-section" ] - (header label :: content) + (sectionTitle label :: content) -header : String -> Html msg -header label = - h3 [ class "sidebar-header" ] [ text label ] +sectionTitle : String -> Html msg +sectionTitle label = + h3 [ class "sidebar-section-title" ] [ text label ] item : msg -> String -> Html msg diff --git a/src/css/app.css b/src/css/app.css index bab39d1..5df5922 100644 --- a/src/css/app.css +++ b/src/css/app.css @@ -52,6 +52,8 @@ --color-tooltip-fg: var(--color-sidebar-tooltip-fg); --color-tooltip-bg: var(--color-sidebar-tooltip-bg); --color-tooltip-border: var(--color-sidebar-tooltip-border); + + --color-main-divider: var(--color-sidebar-divider); } #main-sidebar a:hover { @@ -105,7 +107,7 @@ margin-top: 1rem; } -#main-sidebar .sidebar-header { +#main-sidebar .sidebar-section-title { font-size: var(--font-size-medium); font-weight: normal; color: var(--color-sidebar-subtle-fg); @@ -144,6 +146,10 @@ text-decoration: none; } +#main-sidebar .divider { + margin: 0; +} + /* -- Collapsing ----------------------------------------------------------- */ #main-sidebar .collapse-sidebar-button { @@ -184,23 +190,21 @@ color: var(--color-sidebar-keyboard-shortcut-hover-key-fg); } -/* -- Perspective ---------------------------------------------------------- */ - -#main-sidebar .perspective { - padding: 1rem 1.5rem; - margin-bottom: 0.5rem; +#main-sidebar .sidebar-header { + padding: 1rem 1.5rem 0 1.5rem; display: flex; - flex-direction: row; + flex-direction: column; position: relative; - gap: 0.75rem; + gap: 1.5rem; + margin-bottom: 0.5rem; } -#main-sidebar .perspective:after { +#main-sidebar .sidebar-header:after { position: absolute; left: 1.5rem; right: 1.5rem; bottom: -2rem; - height: 3rem; + height: 1.75rem; content: ""; background: linear-gradient( var(--color-sidebar-bg), @@ -209,11 +213,11 @@ ); } -#main-sidebar .perspective .namespace-slug { +#main-sidebar .sidebar-header .namespace-slug { position: relative; } -#main-sidebar .perspective.is-overflowing .namespace-slug:after { +#main-sidebar .sidebar-header .is-overflowing .namespace-slug:after { position: absolute; top: 0; right: -1.5rem; @@ -228,7 +232,7 @@ ); } -#main-sidebar .perspective .namespace { +#main-sidebar .sidebar-header .namespace { display: inline-flex; color: var(--color-sidebar-fg-em); font-size: 1rem; @@ -240,17 +244,23 @@ flex-direction: row-reverse; } -#main-sidebar .perspective .tooltip-bubble { - width: 16rem; +#main-sidebar .sidebar-header-item { + display: flex; + flex: 1; + flex-direction: row; + user-select: none; + align-items: center; + border-radius: var(--border-radius-base); + height: 1.875rem; + gap: 0.75rem; } -#main-sidebar .perspective .button { - opacity: 0.5; - margin-left: 0.375rem; +#main-sidebar .sidebar-header-item .button { + width: 100%; } -#main-sidebar .perspective .button:hover { - opacity: 1; +#main-sidebar . { + margin-bottom: 0; } /* -- Main Sidebar Unison Submenu ----------------------------------------------------- */ @@ -451,6 +461,7 @@ @import "./help-modal.css"; @import "./publish-modal.css"; @import "./report-bug-modal.css"; +@import "./download-modal.css"; @import "./finder.css"; @import "./perspective-landing.css"; diff --git a/src/css/composites.css b/src/css/composites.css index b2d0303..a87d89d 100644 --- a/src/css/composites.css +++ b/src/css/composites.css @@ -2,3 +2,4 @@ @import "./composites/modal.css"; @import "./composites/codebase-tree.css"; @import "./composites/readme.css"; +@import "./composites/copy-field.css"; diff --git a/src/css/composites/copy-field.css b/src/css/composites/copy-field.css new file mode 100644 index 0000000..f58a509 --- /dev/null +++ b/src/css/composites/copy-field.css @@ -0,0 +1,75 @@ +.copy-field { + position: relative; + display: flex; + flex-direction: row; + height: 2.25rem; + font-family: var(--font-monospace); + + --height-without-border: calc(2.25rem - 2px); +} + +.copy-field .copy-field-field { + position: relative; + display: flex; + flex-direction: row; + background: var(--color-gray-lighten-60); + border: 1px solid var(--color-gray-lighten-40); + border-radius: var(--border-radius-base) 0 0 var(--border-radius-base); + flex-grow: 1; +} + +.copy-field .copy-field-field:focus-within { + box-shadow: 0 0 0 2px var(--color-blue-3); + border-color: var(--color-blue-1); + border-right: 1px solid var(--color-blue-1); + /* z-index is to help cover the left border of the button when focused */ + z-index: 2; +} + +.copy-field .copy-field-prefix { + height: var(--height-without-border); + padding: 0 0.5ch 0 0.5rem; + font-size: var(--font-size-medium); + align-items: center; + display: flex; + color: var(--color-gray-lighten-20); +} + +.copy-field .copy-field-input { + flex-grow: 1; +} + +.copy-field input { + width: 100%; + font-family: var(--font-monospace); + height: var(--height-without-border); + font-size: var(--font-size-medium); + font-weight: 600; + background: transparent; +} + +.copy-field input:focus { + outline: none; +} + +/* TODO: Should this button be more aligned with other buttons in the app? */ +.copy-field button { + width: 2.25rem; + height: 2.25rem; + border: 1px solid var(--color-gray-lighten-40); + border-radius: 0 var(--border-radius-base) var(--border-radius-base) 0; + /* move 1 px left such that borders of field and button overlap + * (visible when clicking the button) */ + margin-left: -1px; + position: relative; +} + +.copy-field button:hover { + border-color: var(--color-gray-lighten-30); + /* z-index is to show the buttons left border on hover */ + z-index: 1; +} + +.copy-field button .icon { + font-size: 2.25rem; +} diff --git a/src/css/composites/modal.css b/src/css/composites/modal.css index 0d8d660..7ac55d1 100644 --- a/src/css/composites/modal.css +++ b/src/css/composites/modal.css @@ -69,3 +69,8 @@ .modal:focus { outline: none; } + +.hint { + font-size: var(--font-size-small); + color: var(--color-modal-subtle-fg-em); +} diff --git a/src/css/download-modal.css b/src/css/download-modal.css new file mode 100644 index 0000000..d6c5c2f --- /dev/null +++ b/src/css/download-modal.css @@ -0,0 +1,12 @@ +#download-modal { + width: 32rem; +} + +#download-modal p { + margin-bottom: 1.5rem; +} + +#download-modal .hint { + margin-top: 0.5rem; + padding-left: calc(var(--border-radius-base) / 2); +} diff --git a/src/css/elements.css b/src/css/elements.css index d6611ab..2265f2f 100644 --- a/src/css/elements.css +++ b/src/css/elements.css @@ -41,7 +41,7 @@ p { margin-bottom: 1em; } -hr { +.divider { background: var(--color-main-divider); border: 0; margin: 1.5rem 0; diff --git a/src/css/themes/unison/light.css b/src/css/themes/unison/light.css index 5c84e29..7d4e8b7 100644 --- a/src/css/themes/unison/light.css +++ b/src/css/themes/unison/light.css @@ -53,6 +53,7 @@ --color-sidebar-button-default-bg: var(--color-gray-base); --color-sidebar-button-default-hover-fg: var(--color-gray-lighten-50); --color-sidebar-button-default-hover-bg: var(--color-gray-base); + --color-sidebar-divider: var(--color-gray-darken-10); --color-sidebar-tooltip-fg: var(--color-gray-lighten-60); --color-sidebar-tooltip-bg: var(--color-gray-darken-30); diff --git a/src/init.js b/src/init.js index 46b2178..fa41c1b 100644 --- a/src/init.js +++ b/src/init.js @@ -1,6 +1,9 @@ import "./css/fonts.css"; import "./css/main.css"; +// Include web components +import "./UI/CopyOnClick"; + console.log(` _____ _ | | |___|_|___ ___ ___