├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── pre-release.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── design │ └── relative-to-changing-fetching-by-name-vs-hash-indexing-and-urls.md ├── elm.json ├── package-lock.json ├── package.json ├── review ├── elm.json ├── src │ └── ReviewConfig.elm └── suppressed │ └── NoUnused.Modules.json ├── src ├── Code │ ├── CodebaseApi.elm │ ├── CodebaseTree.elm │ ├── CodebaseTree │ │ └── NamespaceListing.elm │ ├── Config.elm │ ├── Definition │ │ ├── AbilityConstructor.elm │ │ ├── Category.elm │ │ ├── DataConstructor.elm │ │ ├── Doc.elm │ │ ├── Info.elm │ │ ├── Readme.elm │ │ ├── Reference.elm │ │ ├── Source.elm │ │ ├── Term.elm │ │ └── Type.elm │ ├── Finder.elm │ ├── Finder │ │ ├── FinderMatch.elm │ │ └── SearchOptions.elm │ ├── FullyQualifiedName.elm │ ├── FullyQualifiedNameSet.elm │ ├── Hash.elm │ ├── HashQualified.elm │ ├── Hashvatar.elm │ ├── Hashvatar │ │ └── HexGrid.elm │ ├── Namespace.elm │ ├── Namespace │ │ └── NamespaceRef.elm │ ├── Perspective.elm │ ├── Project.elm │ ├── README.md │ ├── Syntax.elm │ ├── UrlParsers.elm │ ├── Workspace.elm │ └── Workspace │ │ ├── WorkspaceItem.elm │ │ ├── WorkspaceItems.elm │ │ └── Zoom.elm ├── Lib │ ├── Color │ │ └── Harmony.elm │ ├── HttpApi.elm │ ├── OperatingSystem.elm │ ├── README.md │ ├── SearchResults.elm │ ├── TreePath.elm │ ├── Util.elm │ ├── detectOs.js │ └── preventDefaultGlobalKeyboardEvents.js ├── UI.elm ├── UI │ ├── AppHeader.elm │ ├── Banner.elm │ ├── Button.elm │ ├── Card.elm │ ├── Click.elm │ ├── Color.elm │ ├── CopyField.elm │ ├── CopyOnClick.js │ ├── FoldToggle.elm │ ├── Icon.elm │ ├── KeyboardShortcut.elm │ ├── KeyboardShortcut │ │ ├── Key.elm │ │ └── KeyboardEvent.elm │ ├── Modal.elm │ ├── PageLayout.elm │ ├── README.md │ ├── Sidebar.elm │ ├── Toolbar.elm │ └── Tooltip.elm ├── UnisonLocal.elm ├── UnisonLocal │ ├── Api.elm │ ├── App.elm │ ├── Env.elm │ ├── PerspectiveLanding.elm │ ├── PreApp.elm │ └── Route.elm ├── UnisonShare.elm ├── UnisonShare │ ├── Api.elm │ ├── App.elm │ ├── AppModal.elm │ ├── Catalog.elm │ ├── Catalog │ │ └── CatalogMask.elm │ ├── Env.elm │ ├── Log.elm │ ├── Page │ │ ├── CatalogPage.elm │ │ └── UserPage.elm │ ├── PerspectiveLanding.elm │ ├── PreApp.elm │ ├── Route.elm │ └── User.elm ├── assets │ ├── FiraCode │ │ └── FiraCode-VF.woff2 │ ├── Inter │ │ ├── Inter-italic.var.woff2 │ │ ├── Inter-roman.var.woff2 │ │ ├── Inter.var.woff2 │ │ └── LICENSE.txt │ ├── circle-grid-color.svg │ ├── empty-state-bg.svg │ ├── favicon.svg │ └── unison-share-social.png ├── css │ ├── code.css │ ├── code │ │ ├── definition-doc.css │ │ ├── finder.css │ │ ├── fully-qualified-name.css │ │ ├── hashvatar.css │ │ ├── project-listing.css │ │ ├── syntax.css │ │ ├── workspace-item.css │ │ └── workspace.css │ ├── themes │ │ └── unison-light.css │ ├── ui.css │ ├── ui │ │ ├── animations.css │ │ ├── base.css │ │ ├── colors.css │ │ ├── components.css │ │ ├── components │ │ │ ├── app-header.css │ │ │ ├── badges.css │ │ │ ├── button.css │ │ │ ├── card.css │ │ │ ├── codebase-tree.css │ │ │ ├── copy-field.css │ │ │ ├── divider.css │ │ │ ├── fold-toggle.css │ │ │ ├── icon.css │ │ │ ├── keyboard-shortcuts.css │ │ │ ├── loading-placeholder.css │ │ │ ├── modal.css │ │ │ ├── text.css │ │ │ ├── toolbar.css │ │ │ └── tooltip.css │ │ ├── fonts.css │ │ └── page-layout.css │ ├── unison-local.css │ ├── unison-local │ │ ├── help-modal.css │ │ ├── perspective-landing.css │ │ ├── publish-modal.css │ │ └── report-bug-modal.css │ ├── unison-share.css │ └── unison-share │ │ ├── banner.css │ │ ├── download-modal.css │ │ ├── help-modal.css │ │ ├── page │ │ ├── catalog.css │ │ └── user-page.css │ │ ├── perspective-landing.css │ │ ├── publish-modal.css │ │ └── report-bug-modal.css ├── unisonLocal.ejs ├── unisonLocal.js ├── unisonShare.ejs └── unisonShare.js ├── static └── favicon.ico ├── tests ├── Code │ ├── CodebaseTree │ │ └── NamespaceListingTests.elm │ ├── Definition │ │ └── DocTests.elm │ ├── Finder │ │ └── SearchOptionsTests.elm │ ├── FullyQualifiedNameTests.elm │ ├── HashQualifiedTests.elm │ ├── HashTests.elm │ ├── ProjectTests.elm │ └── Workspace │ │ └── WorkspaceItemsTests.elm ├── Lib │ ├── SearchResultsTests.elm │ └── TreePathTests.elm ├── UnisonLocal │ └── RouteTests.elm └── UnisonShare │ ├── Catalog │ └── CatalogMaskTests.elm │ ├── CatalogTests.elm │ └── RouteTests.elm ├── webpack.prod.js ├── webpack.unisonLocal.dev.js └── webpack.unisonShare.dev.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Problem 2 | Describe the problem this PR intend to solve 3 | 4 | ## Solution 5 | Describe the solution this PR takes to solve the problem and any alternative approach considered 6 | 7 | ## Caveats/Notes 8 | Issues that address things you didn't get to or general notes 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 15.x] 17 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - run: npm ci 26 | - run: npm run build --if-present 27 | - run: npm test 28 | - run: npm run review 29 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: "pre-release" 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | branches: [ main ] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | pre-release: 12 | name: "Pre Release" 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm run build 24 | - uses: "marvinpinto/action-automatic-releases@latest" 25 | with: 26 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 27 | automatic_release_tag: "latest" 28 | prerelease: true 29 | title: "Development Build" 30 | files: dist/*.zip 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | .DS_Store 3 | public/bundle.js 4 | node_modules 5 | dist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Unison Computing, public benefit corp and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Unison Codebase UI 2 | ================== 3 | This is now archived version of the UI. 4 | [Unison Local UI](https://github.com/unisonweb/unison-local-ui) and [UI Core](https://github.com/unisonweb/ui-core) has replaced this repository. 5 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "NoRedInk/elm-simple-fuzzy": "1.0.3", 10 | "avh4/elm-color": "1.0.0", 11 | "elm/browser": "1.0.2", 12 | "elm/core": "1.0.5", 13 | "elm/html": "1.0.0", 14 | "elm/http": "2.0.0", 15 | "elm/json": "1.1.3", 16 | "elm/parser": "1.1.0", 17 | "elm/regex": "1.0.0", 18 | "elm/svg": "1.0.1", 19 | "elm/url": "1.0.0", 20 | "elm-community/html-extra": "3.4.0", 21 | "elm-community/json-extra": "4.3.0", 22 | "elm-community/list-extra": "8.2.4", 23 | "elm-community/maybe-extra": "5.2.0", 24 | "elm-community/string-extra": "4.0.1", 25 | "j-maas/elm-ordered-containers": "1.0.0", 26 | "krisajenkins/remotedata": "6.0.1", 27 | "mgold/elm-nonempty-list": "4.1.0", 28 | "noahzgordon/elm-color-extra": "1.0.2", 29 | "stoeffel/set-extra": "1.2.3", 30 | "wernerdegroot/listzipper": "4.0.0" 31 | }, 32 | "indirect": { 33 | "elm/bytes": "1.0.8", 34 | "elm/file": "1.0.5", 35 | "elm/random": "1.0.0", 36 | "elm/time": "1.0.0", 37 | "elm/virtual-dom": "1.0.2", 38 | "fredcy/elm-parseint": "2.0.1", 39 | "rtfeldman/elm-iso8601-date-strings": "1.1.3" 40 | } 41 | }, 42 | "test-dependencies": { 43 | "direct": { 44 | "elm-explorations/test": "1.2.2" 45 | }, 46 | "indirect": {} 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codebase-ui", 3 | "version": "1.0.0", 4 | "description": "Unison Codebase UI", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/unisonweb/codebase-ui.git" 8 | }, 9 | "scripts": { 10 | "build": "webpack --mode production --config webpack.prod.js", 11 | "clean": "rm -rf dist", 12 | "start": "echo 'Please use either `npm run start:unisonLocal` or `npm run start:unisonShare`'", 13 | "start:unisonLocal": "webpack serve --mode development --port 1234 --config webpack.unisonLocal.dev.js", 14 | "start:unisonShare": "webpack serve --mode development --port 1234 --config webpack.unisonShare.dev.js", 15 | "test": "elm-test", 16 | "review": "elm-review" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/unisonweb/codebase-ui/issues" 20 | }, 21 | "homepage": "https://github.com/unisonweb/codebase-ui#readme", 22 | "devDependencies": { 23 | "archiver": "^5.3.0", 24 | "copy-webpack-plugin": "^8.1.1", 25 | "css-loader": "^5.2.4", 26 | "elm": "^0.19.1-5", 27 | "elm-asset-webpack-loader": "^1.1.2", 28 | "elm-format": "^0.8.5", 29 | "elm-review": "^2.5.5", 30 | "elm-test": "^0.19.1-revision6", 31 | "elm-webpack-loader": "^8.0.0", 32 | "filemanager-webpack-plugin": "^4.0.0", 33 | "html-webpack-plugin": "^5.3.1", 34 | "style-loader": "^2.0.0", 35 | "webpack": "^5.66.0", 36 | "webpack-cli": "^4.9.1", 37 | "webpack-dev-server": "^4.7.3" 38 | }, 39 | "engines": { 40 | "node": ">=12.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /review/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.5", 10 | "jfmengels/elm-review": "2.5.0", 11 | "jfmengels/elm-review-simplify": "2.0.7", 12 | "jfmengels/elm-review-unused": "1.1.16", 13 | "stil4m/elm-syntax": "7.2.7" 14 | }, 15 | "indirect": { 16 | "elm/html": "1.0.0", 17 | "elm/json": "1.1.3", 18 | "elm/parser": "1.1.0", 19 | "elm/project-metadata-utils": "1.0.2", 20 | "elm/random": "1.0.0", 21 | "elm/time": "1.0.0", 22 | "elm/virtual-dom": "1.0.2", 23 | "elm-community/list-extra": "8.4.0", 24 | "elm-explorations/test": "1.2.2", 25 | "miniBill/elm-unicode": "1.0.2", 26 | "rtfeldman/elm-hex": "1.0.0", 27 | "stil4m/structured-writer": "1.0.3" 28 | } 29 | }, 30 | "test-dependencies": { 31 | "direct": { 32 | "elm-explorations/test": "1.2.2" 33 | }, 34 | "indirect": {} 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /review/src/ReviewConfig.elm: -------------------------------------------------------------------------------- 1 | module ReviewConfig exposing (config) 2 | 3 | import NoUnused.Dependencies 4 | import NoUnused.Modules 5 | import NoUnused.Parameters 6 | import NoUnused.Patterns 7 | import NoUnused.Variables 8 | import Review.Rule exposing (Rule) 9 | import Simplify 10 | 11 | 12 | config : List Rule 13 | config = 14 | [ NoUnused.Dependencies.rule 15 | , NoUnused.Modules.rule 16 | , NoUnused.Parameters.rule 17 | , NoUnused.Patterns.rule 18 | , NoUnused.Variables.rule 19 | , Simplify.rule Simplify.defaults 20 | ] 21 | -------------------------------------------------------------------------------- /review/suppressed/NoUnused.Modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "automatically created by": "elm-review suppress", 4 | "learn more": "elm-review suppress --help", 5 | "suppressions": [ 6 | { "count": 1, "filePath": "src/UnisonShare/Log.elm" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/Code/CodebaseApi.elm: -------------------------------------------------------------------------------- 1 | module Code.CodebaseApi exposing (..) 2 | 3 | import Code.Definition.Reference exposing (Reference) 4 | import Code.FullyQualifiedName exposing (FQN) 5 | import Code.Namespace.NamespaceRef exposing (NamespaceRef) 6 | import Code.Perspective exposing (Perspective) 7 | import Code.Syntax as Syntax 8 | import Lib.HttpApi as HttpApi 9 | 10 | 11 | 12 | {- 13 | 14 | CodebaseApi 15 | =========== 16 | 17 | The CodebaseApi module, describes the endpoints used for the Code library to 18 | connect to a codebase, but is merely that; a description. Consumers of the Code 19 | library will need to provide an implementation of `ToApiEndpointUrl` in order 20 | to perform the actual HTTP requests. 21 | 22 | -} 23 | 24 | 25 | type CodebaseEndpoint 26 | = Find 27 | { perspective : Perspective 28 | , withinFqn : Maybe FQN 29 | , limit : Int 30 | , sourceWidth : Syntax.Width 31 | , query : String 32 | } 33 | | Browse { perspective : Perspective, ref : Maybe NamespaceRef } 34 | | Definition { perspective : Perspective, ref : Reference } 35 | 36 | 37 | type alias ToApiEndpointUrl = 38 | CodebaseEndpoint -> HttpApi.EndpointUrl 39 | -------------------------------------------------------------------------------- /src/Code/Config.elm: -------------------------------------------------------------------------------- 1 | module Code.Config exposing (..) 2 | 3 | import Code.CodebaseApi exposing (ToApiEndpointUrl) 4 | import Code.Perspective exposing (Perspective) 5 | import Lib.HttpApi exposing (ApiBasePath) 6 | import Lib.OperatingSystem exposing (OperatingSystem) 7 | 8 | 9 | type alias Config = 10 | { operatingSystem : OperatingSystem 11 | , perspective : Perspective 12 | , toApiEndpointUrl : ToApiEndpointUrl 13 | , apiBasePath : ApiBasePath 14 | } 15 | -------------------------------------------------------------------------------- /src/Code/Definition/AbilityConstructor.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.AbilityConstructor exposing 2 | ( AbilityConstructor(..) 3 | , AbilityConstructorDetail 4 | , AbilityConstructorListing 5 | , AbilityConstructorSource(..) 6 | , AbilityConstructorSummary 7 | , decodeSignature 8 | , decodeSource 9 | ) 10 | 11 | import Code.Definition.Info exposing (Info) 12 | import Code.Definition.Term as Term exposing (TermSignature) 13 | import Code.Definition.Type as Type exposing (TypeSource) 14 | import Code.FullyQualifiedName exposing (FQN) 15 | import Code.Hash exposing (Hash) 16 | import Code.Syntax exposing (Syntax) 17 | import Json.Decode as Decode 18 | 19 | 20 | type AbilityConstructorSource 21 | = Source Syntax 22 | | Builtin 23 | 24 | 25 | type AbilityConstructor a 26 | = AbilityConstructor Hash a 27 | 28 | 29 | type alias AbilityConstructorDetail = 30 | AbilityConstructor { info : Info, source : TypeSource } 31 | 32 | 33 | type alias AbilityConstructorSummary = 34 | AbilityConstructor 35 | { fqn : FQN 36 | , name : FQN 37 | , namespace : Maybe String 38 | , signature : TermSignature 39 | } 40 | 41 | 42 | type alias AbilityConstructorListing = 43 | AbilityConstructor FQN 44 | 45 | 46 | 47 | -- JSON DECODERS 48 | 49 | 50 | decodeSource : List String -> List String -> Decode.Decoder TypeSource 51 | decodeSource = 52 | Type.decodeTypeSource 53 | 54 | 55 | decodeSignature : List String -> Decode.Decoder TermSignature 56 | decodeSignature = 57 | Term.decodeSignature 58 | -------------------------------------------------------------------------------- /src/Code/Definition/Category.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.Category exposing (..) 2 | 3 | import Code.Definition.Term exposing (TermCategory(..)) 4 | import Code.Definition.Type exposing (TypeCategory(..)) 5 | import UI.Icon as Icon exposing (Icon) 6 | 7 | 8 | type Category 9 | = Type TypeCategory 10 | | Term TermCategory 11 | 12 | 13 | name : Category -> String 14 | name category = 15 | case category of 16 | Type c -> 17 | case c of 18 | DataType -> 19 | "type" 20 | 21 | AbilityType -> 22 | "ability" 23 | 24 | Term c -> 25 | case c of 26 | PlainTerm -> 27 | "term" 28 | 29 | TestTerm -> 30 | "test" 31 | 32 | DocTerm -> 33 | "doc" 34 | 35 | 36 | icon : Category -> Icon msg 37 | icon category = 38 | case category of 39 | Type c -> 40 | case c of 41 | DataType -> 42 | Icon.type_ 43 | 44 | AbilityType -> 45 | Icon.ability 46 | 47 | Term c -> 48 | case c of 49 | PlainTerm -> 50 | Icon.term 51 | 52 | TestTerm -> 53 | Icon.test 54 | 55 | DocTerm -> 56 | Icon.doc 57 | -------------------------------------------------------------------------------- /src/Code/Definition/DataConstructor.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.DataConstructor exposing 2 | ( DataConstructor(..) 3 | , DataConstructorDetail 4 | , DataConstructorListing 5 | , DataConstructorSource(..) 6 | , DataConstructorSummary 7 | , decodeSignature 8 | , decodeSource 9 | ) 10 | 11 | import Code.Definition.Info exposing (Info) 12 | import Code.Definition.Term as Term exposing (TermSignature) 13 | import Code.Definition.Type as Type exposing (TypeSource) 14 | import Code.FullyQualifiedName exposing (FQN) 15 | import Code.Hash exposing (Hash) 16 | import Code.Syntax exposing (Syntax) 17 | import Json.Decode as Decode 18 | 19 | 20 | type DataConstructorSource 21 | = Source Syntax 22 | | Builtin 23 | 24 | 25 | type DataConstructor a 26 | = DataConstructor Hash a 27 | 28 | 29 | type alias DataConstructorDetail = 30 | DataConstructor { info : Info, source : TypeSource } 31 | 32 | 33 | type alias DataConstructorSummary = 34 | DataConstructor 35 | { fqn : FQN 36 | , name : FQN 37 | , namespace : Maybe String 38 | , signature : TermSignature 39 | } 40 | 41 | 42 | type alias DataConstructorListing = 43 | DataConstructor FQN 44 | 45 | 46 | 47 | -- JSON DECODERS 48 | 49 | 50 | decodeSource : List String -> List String -> Decode.Decoder TypeSource 51 | decodeSource = 52 | Type.decodeTypeSource 53 | 54 | 55 | decodeSignature : List String -> Decode.Decoder TermSignature 56 | decodeSignature = 57 | Term.decodeSignature 58 | -------------------------------------------------------------------------------- /src/Code/Definition/Info.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.Info exposing (..) 2 | 3 | import Code.Definition.Reference as Reference exposing (Reference) 4 | import Code.FullyQualifiedName as FQN exposing (FQN) 5 | import List.Extra as ListE 6 | import List.Nonempty as NEL 7 | 8 | 9 | 10 | -- TODO: Without `otherNames` and if the `FQN` was added, 11 | -- `Info` would be translatable to 12 | -- `TermSummary`/`TypeSummary`. 13 | -- Perhaps it should be `Naming` instead? 14 | -- `otherNames` is a detail thing only. 15 | 16 | 17 | type alias Info = 18 | { name : FQN 19 | , namespace : Maybe FQN 20 | , otherNames : List FQN 21 | } 22 | 23 | 24 | makeInfo : Reference -> FQN -> NEL.Nonempty FQN -> Info 25 | makeInfo requestedRef suffixName allFqns = 26 | let 27 | ( namespace, otherNames ) = 28 | namespaceAndOtherNames requestedRef suffixName allFqns 29 | in 30 | Info suffixName namespace otherNames 31 | 32 | 33 | 34 | -- Helpers 35 | 36 | 37 | namespaceAndOtherNames : Reference -> FQN -> NEL.Nonempty FQN -> ( Maybe FQN, List FQN ) 38 | namespaceAndOtherNames requestedRef suffixName fqns = 39 | let 40 | shortest = 41 | NEL.sortBy FQN.numSegments >> NEL.head 42 | 43 | shortestSuffixMatching = 44 | let 45 | defaultFqn = 46 | shortest fqns 47 | in 48 | fqns 49 | |> NEL.filter (FQN.isSuffixOf suffixName) defaultFqn 50 | |> shortest 51 | 52 | fqnWithin = 53 | case Reference.fqn requestedRef of 54 | Just requestedName -> 55 | if FQN.isSuffixOf suffixName requestedName then 56 | requestedName 57 | 58 | else 59 | shortestSuffixMatching 60 | 61 | Nothing -> 62 | shortestSuffixMatching 63 | 64 | fqnsWithout = 65 | fqns 66 | |> NEL.toList 67 | |> ListE.filterNot (FQN.equals fqnWithin) 68 | in 69 | ( FQN.namespace fqnWithin, fqnsWithout ) 70 | -------------------------------------------------------------------------------- /src/Code/Definition/Readme.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.Readme exposing (..) 2 | 3 | import Code.Definition.Doc as Doc exposing (Doc, DocFoldToggles, FoldId) 4 | import Code.Definition.Reference exposing (Reference) 5 | import Html exposing (Html, div) 6 | import Html.Attributes exposing (class) 7 | import Json.Decode as Decode 8 | 9 | 10 | {-| Represent the Readme Doc definition of a namespace. This is typically 11 | rendered slightly different than other docs when viewed from a Namespace 12 | landing page point of view. 13 | -} 14 | type Readme 15 | = Readme Doc 16 | 17 | 18 | 19 | -- VIEW 20 | 21 | 22 | view : 23 | (Reference -> msg) 24 | -> (FoldId -> msg) 25 | -> DocFoldToggles 26 | -> Readme 27 | -> Html msg 28 | view refToMsg toggleFoldMsg docFoldToggles (Readme doc) = 29 | div [ class "readme" ] 30 | [ Doc.view refToMsg toggleFoldMsg docFoldToggles doc ] 31 | 32 | 33 | 34 | -- DECODE 35 | 36 | 37 | decode : Decode.Decoder Readme 38 | decode = 39 | Decode.map Readme Doc.decode 40 | -------------------------------------------------------------------------------- /src/Code/Definition/Reference.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.Reference exposing (..) 2 | 3 | import Code.FullyQualifiedName exposing (FQN) 4 | import Code.Hash exposing (Hash) 5 | import Code.HashQualified as HQ exposing (HashQualified) 6 | import UI.Icon as Icon exposing (Icon) 7 | import Url.Parser 8 | 9 | 10 | type Reference 11 | = TermReference HashQualified 12 | | TypeReference HashQualified 13 | | AbilityConstructorReference HashQualified 14 | | DataConstructorReference HashQualified 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | fromString : (HashQualified -> Reference) -> String -> Reference 22 | fromString toRef str = 23 | str |> HQ.fromString |> toRef 24 | 25 | 26 | fromUrlString : (HashQualified -> Reference) -> String -> Reference 27 | fromUrlString toRef str = 28 | str |> HQ.fromUrlString |> toRef 29 | 30 | 31 | urlParser : (HashQualified -> Reference) -> Url.Parser.Parser (Reference -> a) a 32 | urlParser toRef = 33 | Url.Parser.map toRef HQ.urlParser 34 | 35 | 36 | 37 | -- HELPERS 38 | 39 | 40 | equals : Reference -> Reference -> Bool 41 | equals a b = 42 | case ( a, b ) of 43 | ( TermReference aHq, TermReference bHq ) -> 44 | HQ.equals aHq bHq 45 | 46 | ( TypeReference aHq, TypeReference bHq ) -> 47 | HQ.equals aHq bHq 48 | 49 | ( AbilityConstructorReference aHq, AbilityConstructorReference bHq ) -> 50 | HQ.equals aHq bHq 51 | 52 | ( DataConstructorReference aHq, DataConstructorReference bHq ) -> 53 | HQ.equals aHq bHq 54 | 55 | _ -> 56 | False 57 | 58 | 59 | {-| Like `equals`, but compares deeper such that a HashQualified with the same 60 | Hash as a HashOnly are considered the same 61 | -} 62 | same : Reference -> Reference -> Bool 63 | same a b = 64 | case ( a, b ) of 65 | ( TermReference aHq, TermReference bHq ) -> 66 | HQ.same aHq bHq 67 | 68 | ( TypeReference aHq, TypeReference bHq ) -> 69 | HQ.same aHq bHq 70 | 71 | ( AbilityConstructorReference aHq, AbilityConstructorReference bHq ) -> 72 | HQ.same aHq bHq 73 | 74 | ( DataConstructorReference aHq, DataConstructorReference bHq ) -> 75 | HQ.same aHq bHq 76 | 77 | _ -> 78 | False 79 | 80 | 81 | hashQualified : Reference -> HashQualified 82 | hashQualified ref = 83 | case ref of 84 | TermReference hq -> 85 | hq 86 | 87 | TypeReference hq -> 88 | hq 89 | 90 | AbilityConstructorReference hq -> 91 | hq 92 | 93 | DataConstructorReference hq -> 94 | hq 95 | 96 | 97 | fqn : Reference -> Maybe FQN 98 | fqn = 99 | hashQualified >> HQ.name 100 | 101 | 102 | hash : Reference -> Maybe Hash 103 | hash = 104 | hashQualified >> HQ.hash 105 | 106 | 107 | 108 | -- TRANSFORM 109 | 110 | 111 | toString : Reference -> String 112 | toString ref = 113 | case ref of 114 | TermReference hq -> 115 | "term__" ++ HQ.toString hq 116 | 117 | TypeReference hq -> 118 | "type__" ++ HQ.toString hq 119 | 120 | AbilityConstructorReference hq -> 121 | "ability_constructor__" ++ HQ.toString hq 122 | 123 | DataConstructorReference hq -> 124 | "data_constructor__" ++ HQ.toString hq 125 | 126 | 127 | {-| toApiUrlString is lossy in that the kind of definition is lost in the 128 | serialization 129 | -} 130 | toApiUrlString : Reference -> String 131 | toApiUrlString ref = 132 | case ref of 133 | TermReference hq -> 134 | HQ.toString hq 135 | 136 | TypeReference hq -> 137 | HQ.toString hq 138 | 139 | AbilityConstructorReference hq -> 140 | HQ.toString hq 141 | 142 | DataConstructorReference hq -> 143 | HQ.toString hq 144 | 145 | 146 | toHumanString : Reference -> String 147 | toHumanString ref = 148 | case ref of 149 | TermReference hq -> 150 | "Term " ++ HQ.toString hq 151 | 152 | TypeReference hq -> 153 | "Type " ++ HQ.toString hq 154 | 155 | AbilityConstructorReference hq -> 156 | "Ability constructor " ++ HQ.toString hq 157 | 158 | DataConstructorReference hq -> 159 | "Data Constructor " ++ HQ.toString hq 160 | 161 | 162 | toIcon : Reference -> Icon msg 163 | toIcon ref = 164 | case ref of 165 | TermReference _ -> 166 | Icon.term 167 | 168 | TypeReference _ -> 169 | Icon.type_ 170 | 171 | AbilityConstructorReference _ -> 172 | Icon.abilityConstructor 173 | 174 | DataConstructorReference _ -> 175 | Icon.dataConstructor 176 | 177 | 178 | map : (HashQualified -> HashQualified) -> Reference -> Reference 179 | map f ref = 180 | case ref of 181 | TermReference hq -> 182 | TermReference (f hq) 183 | 184 | TypeReference hq -> 185 | TypeReference (f hq) 186 | 187 | AbilityConstructorReference hq -> 188 | AbilityConstructorReference (f hq) 189 | 190 | DataConstructorReference hq -> 191 | DataConstructorReference (f hq) 192 | -------------------------------------------------------------------------------- /src/Code/Definition/Source.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.Source exposing 2 | ( Source(..) 3 | , ViewConfig(..) 4 | , isBuiltin 5 | , numTermLines 6 | , numTermSignatureLines 7 | , numTypeLines 8 | , view 9 | , viewNamedTermSignature 10 | , viewTermSignature 11 | , viewTermSource 12 | , viewTypeSource 13 | ) 14 | 15 | import Code.Definition.Reference exposing (Reference) 16 | import Code.Definition.Term as Term exposing (TermSignature(..), TermSource) 17 | import Code.Definition.Type as Type exposing (TypeSource) 18 | import Code.FullyQualifiedName as FQN exposing (FQN) 19 | import Code.Syntax as Syntax 20 | import Html exposing (Html, span, text) 21 | import Html.Attributes exposing (class) 22 | import UI 23 | 24 | 25 | type ViewConfig msg 26 | = Rich (Reference -> msg) 27 | | Monochrome 28 | | Plain 29 | 30 | 31 | type 32 | Source 33 | -- Term name source 34 | = Term FQN TermSource 35 | | Type TypeSource 36 | 37 | 38 | 39 | -- HELPERS 40 | 41 | 42 | isBuiltin : Source -> Bool 43 | isBuiltin source = 44 | case source of 45 | Type Type.Builtin -> 46 | True 47 | 48 | Term _ (Term.Builtin _) -> 49 | True 50 | 51 | _ -> 52 | False 53 | 54 | 55 | numTypeLines : TypeSource -> Int 56 | numTypeLines source = 57 | case source of 58 | Type.Source syntax -> 59 | Syntax.numLines syntax 60 | 61 | Type.Builtin -> 62 | 1 63 | 64 | 65 | numTermLines : TermSource -> Int 66 | numTermLines source = 67 | case source of 68 | Term.Source _ syntax -> 69 | Syntax.numLines syntax 70 | 71 | Term.Builtin (TermSignature signature) -> 72 | Syntax.numLines signature 73 | 74 | 75 | numTermSignatureLines : TermSource -> Int 76 | numTermSignatureLines source = 77 | case source of 78 | Term.Source (TermSignature signature) _ -> 79 | Syntax.numLines signature 80 | 81 | Term.Builtin (TermSignature signature) -> 82 | Syntax.numLines signature 83 | 84 | 85 | 86 | -- VIEW 87 | 88 | 89 | view : ViewConfig msg -> Source -> Html msg 90 | view viewConfig source = 91 | case source of 92 | Type typeSource -> 93 | viewTypeSource viewConfig typeSource 94 | 95 | Term termName termSource -> 96 | viewTermSource viewConfig termName termSource 97 | 98 | 99 | viewTypeSource : ViewConfig msg -> TypeSource -> Html msg 100 | viewTypeSource viewConfig source = 101 | let 102 | content = 103 | case source of 104 | Type.Source syntax -> 105 | viewSyntax viewConfig syntax 106 | 107 | Type.Builtin -> 108 | span 109 | [] 110 | [ span [] [ text "builtin " ] 111 | , span [ class "data-type-keyword" ] [ text "type" ] 112 | ] 113 | in 114 | viewCode viewConfig content 115 | 116 | 117 | viewTermSignature : ViewConfig msg -> TermSignature -> Html msg 118 | viewTermSignature viewConfig (TermSignature syntax) = 119 | viewCode viewConfig (viewSyntax viewConfig syntax) 120 | 121 | 122 | viewNamedTermSignature : ViewConfig msg -> FQN -> TermSignature -> Html msg 123 | viewNamedTermSignature viewConfig termName signature = 124 | viewCode viewConfig (viewNamedTermSignature_ viewConfig termName signature) 125 | 126 | 127 | viewNamedTermSignature_ : ViewConfig msg -> FQN -> TermSignature -> Html msg 128 | viewNamedTermSignature_ viewConfig termName (TermSignature syntax) = 129 | span 130 | [] 131 | [ span [ class "hash-qualifier" ] [ text (FQN.toString termName) ] 132 | , span [ class "type-ascription-colon" ] [ text " : " ] 133 | , viewSyntax viewConfig syntax 134 | ] 135 | 136 | 137 | viewTermSource : ViewConfig msg -> FQN -> TermSource -> Html msg 138 | viewTermSource viewConfig termName source = 139 | let 140 | content = 141 | case source of 142 | Term.Source _ syntax -> 143 | viewSyntax viewConfig syntax 144 | 145 | Term.Builtin signature -> 146 | viewNamedTermSignature viewConfig termName signature 147 | in 148 | viewCode viewConfig content 149 | 150 | 151 | 152 | -- VIEW HELPERS 153 | 154 | 155 | viewCode : ViewConfig msg -> Html msg -> Html msg 156 | viewCode viewConfig content = 157 | UI.codeBlock 158 | [ class (viewConfigToClassName viewConfig) ] 159 | content 160 | 161 | 162 | viewConfigToClassName : ViewConfig msg -> String 163 | viewConfigToClassName viewConfig = 164 | case viewConfig of 165 | Rich _ -> 166 | "rich" 167 | 168 | Monochrome -> 169 | "monochrome" 170 | 171 | Plain -> 172 | "plain" 173 | 174 | 175 | viewSyntax : ViewConfig msg -> (Syntax.Syntax -> Html msg) 176 | viewSyntax viewConfig = 177 | Syntax.view (viewConfigToSyntaxLinked viewConfig) 178 | 179 | 180 | viewConfigToSyntaxLinked : ViewConfig msg -> Syntax.Linked msg 181 | viewConfigToSyntaxLinked viewConfig = 182 | case viewConfig of 183 | Rich toReferenceClickMsg -> 184 | Syntax.Linked toReferenceClickMsg 185 | 186 | _ -> 187 | Syntax.NotLinked 188 | -------------------------------------------------------------------------------- /src/Code/Definition/Term.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.Term exposing 2 | ( Term(..) 3 | , TermCategory(..) 4 | , TermDetail 5 | , TermListing 6 | , TermSignature(..) 7 | , TermSource(..) 8 | , TermSummary 9 | , decodeSignature 10 | , decodeTermCategory 11 | , decodeTermSource 12 | , isBuiltin 13 | , isBuiltinSource 14 | , termSignature 15 | ) 16 | 17 | import Code.Definition.Info exposing (Info) 18 | import Code.FullyQualifiedName exposing (FQN) 19 | import Code.Hash exposing (Hash) 20 | import Code.Syntax as Syntax exposing (Syntax) 21 | import Json.Decode as Decode exposing (at, string) 22 | import Json.Decode.Extra exposing (when) 23 | 24 | 25 | type TermCategory 26 | = PlainTerm 27 | | TestTerm 28 | | DocTerm 29 | 30 | 31 | type TermSource 32 | = Source TermSignature Syntax 33 | | Builtin TermSignature 34 | 35 | 36 | type Term a 37 | = Term Hash TermCategory a 38 | 39 | 40 | type TermSignature 41 | = TermSignature Syntax 42 | 43 | 44 | type alias TermDetailFields d = 45 | { d | info : Info, source : TermSource } 46 | 47 | 48 | type alias TermDetail d = 49 | Term (TermDetailFields d) 50 | 51 | 52 | type alias TermSummary = 53 | Term 54 | { fqn : FQN 55 | , name : FQN 56 | , namespace : Maybe String 57 | , signature : TermSignature 58 | } 59 | 60 | 61 | type alias TermListing = 62 | Term FQN 63 | 64 | 65 | 66 | -- HELPERS 67 | 68 | 69 | isBuiltin : TermDetail d -> Bool 70 | isBuiltin (Term _ _ d) = 71 | isBuiltinSource d.source 72 | 73 | 74 | isBuiltinSource : TermSource -> Bool 75 | isBuiltinSource source = 76 | case source of 77 | Source _ _ -> 78 | False 79 | 80 | Builtin _ -> 81 | True 82 | 83 | 84 | termSignature : TermSource -> TermSignature 85 | termSignature source = 86 | case source of 87 | Source sig _ -> 88 | sig 89 | 90 | Builtin sig -> 91 | sig 92 | 93 | 94 | 95 | -- JSON DECODERS 96 | 97 | 98 | decodeTermCategory : List String -> Decode.Decoder TermCategory 99 | decodeTermCategory tagPath = 100 | let 101 | tag = 102 | at tagPath string 103 | in 104 | Decode.oneOf 105 | [ when tag ((==) "Test") (Decode.succeed TestTerm) 106 | , when tag ((==) "Doc") (Decode.succeed DocTerm) 107 | , Decode.succeed PlainTerm 108 | ] 109 | 110 | 111 | decodeSignature : List String -> Decode.Decoder TermSignature 112 | decodeSignature signaturePath = 113 | Decode.map TermSignature (at signaturePath Syntax.decode) 114 | 115 | 116 | decodeTermSource : List String -> List String -> List String -> Decode.Decoder TermSource 117 | decodeTermSource tagPath signaturePath sourcePath = 118 | let 119 | tag = 120 | at tagPath string 121 | 122 | decodeUserObject = 123 | Decode.map2 Source 124 | (decodeSignature signaturePath) 125 | (at sourcePath Syntax.decode) 126 | 127 | decodeBuiltin = 128 | Decode.map Builtin (decodeSignature signaturePath) 129 | in 130 | Decode.oneOf 131 | [ when tag ((==) "UserObject") decodeUserObject 132 | , when tag ((==) "BuiltinObject") decodeBuiltin 133 | ] 134 | -------------------------------------------------------------------------------- /src/Code/Definition/Type.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.Type exposing 2 | ( Type(..) 3 | , TypeCategory(..) 4 | , TypeDetail 5 | , TypeListing 6 | , TypeSource(..) 7 | , TypeSummary 8 | , decodeTypeCategory 9 | , decodeTypeSource 10 | , isBuiltin 11 | , isBuiltinSource 12 | ) 13 | 14 | import Code.Definition.Info exposing (Info) 15 | import Code.FullyQualifiedName exposing (FQN) 16 | import Code.Hash exposing (Hash) 17 | import Code.Syntax as Syntax exposing (Syntax) 18 | import Json.Decode as Decode exposing (at, string) 19 | import Json.Decode.Extra exposing (when) 20 | 21 | 22 | type TypeCategory 23 | = DataType 24 | | AbilityType 25 | 26 | 27 | type TypeSource 28 | = Source Syntax 29 | | Builtin 30 | 31 | 32 | type Type a 33 | = Type Hash TypeCategory a 34 | 35 | 36 | type alias TypeDetailFields d = 37 | { d | info : Info, source : TypeSource } 38 | 39 | 40 | type alias TypeDetail d = 41 | Type (TypeDetailFields d) 42 | 43 | 44 | type alias TypeSummary = 45 | Type 46 | { fqn : FQN 47 | , name : FQN 48 | , namespace : Maybe String 49 | , source : TypeSource 50 | } 51 | 52 | 53 | type alias TypeListing = 54 | Type FQN 55 | 56 | 57 | 58 | -- HELPERS 59 | 60 | 61 | isBuiltin : TypeDetail d -> Bool 62 | isBuiltin (Type _ _ d) = 63 | isBuiltinSource d.source 64 | 65 | 66 | isBuiltinSource : TypeSource -> Bool 67 | isBuiltinSource source = 68 | case source of 69 | Source _ -> 70 | False 71 | 72 | Builtin -> 73 | True 74 | 75 | 76 | 77 | -- JSON DECODERS 78 | 79 | 80 | decodeTypeCategory : List String -> Decode.Decoder TypeCategory 81 | decodeTypeCategory tagPath = 82 | let 83 | tag = 84 | at tagPath string 85 | in 86 | Decode.oneOf 87 | [ when tag ((==) "Data") (Decode.succeed DataType) 88 | , when tag ((==) "Ability") (Decode.succeed AbilityType) 89 | ] 90 | 91 | 92 | decodeTypeSource : List String -> List String -> Decode.Decoder TypeSource 93 | decodeTypeSource tagPath sourcePath = 94 | let 95 | tag = 96 | at tagPath string 97 | 98 | decodeUserObject = 99 | Decode.map Source (at sourcePath Syntax.decode) 100 | in 101 | Decode.oneOf 102 | [ when tag ((==) "UserObject") decodeUserObject 103 | , when tag ((==) "BuiltinObject") (Decode.succeed Builtin) 104 | ] 105 | -------------------------------------------------------------------------------- /src/Code/Finder/SearchOptions.elm: -------------------------------------------------------------------------------- 1 | module Code.Finder.SearchOptions exposing (..) 2 | 3 | import Code.FullyQualifiedName as FQN exposing (FQN) 4 | import Code.Perspective as Perspective exposing (Perspective) 5 | import Html exposing (Html, div, span, text) 6 | import Html.Attributes exposing (class) 7 | import UI 8 | 9 | 10 | type WithinOption 11 | = AllNamespaces 12 | -- WithinNamespacePerspective has an FQN to make it more convenient 13 | -- (by avoiding having to pass the Perspective through the view layer) 14 | -- It is meant to be the same FQN that is in Perspective.Namespace. 15 | | WithinNamespacePerspective FQN 16 | | WithinNamespace FQN 17 | 18 | 19 | type SearchOptions 20 | = SearchOptions WithinOption 21 | 22 | 23 | init : Perspective -> Maybe FQN -> SearchOptions 24 | init perspective mfqn = 25 | case ( perspective, mfqn ) of 26 | ( Perspective.Namespace { fqn }, Nothing ) -> 27 | SearchOptions (WithinNamespacePerspective fqn) 28 | 29 | ( _, Just fqn ) -> 30 | SearchOptions (WithinNamespace fqn) 31 | 32 | _ -> 33 | SearchOptions AllNamespaces 34 | 35 | 36 | {-| Removing WithinNamespace when the Perspective is a Namespace perspective 37 | pops back to the WithinNamespacePerspective option (which in turn can also be 38 | removed) 39 | -} 40 | removeWithin : Perspective -> SearchOptions -> SearchOptions 41 | removeWithin perspective (SearchOptions within) = 42 | let 43 | nextWithin = 44 | case ( within, perspective ) of 45 | ( WithinNamespace _, Perspective.Namespace { fqn } ) -> 46 | WithinNamespacePerspective fqn 47 | 48 | _ -> 49 | AllNamespaces 50 | in 51 | SearchOptions nextWithin 52 | 53 | 54 | view : msg -> SearchOptions -> Html msg 55 | view removeWithinMsg (SearchOptions within) = 56 | let 57 | viewWithin fqn = 58 | div [ class "finder-search-options" ] 59 | [ UI.optionBadge 60 | removeWithinMsg 61 | (span [] [ UI.subtle "within ", text (FQN.toString fqn) ]) 62 | ] 63 | in 64 | case within of 65 | AllNamespaces -> 66 | UI.nothing 67 | 68 | WithinNamespacePerspective fqn -> 69 | viewWithin fqn 70 | 71 | WithinNamespace fqn -> 72 | viewWithin fqn 73 | -------------------------------------------------------------------------------- /src/Code/FullyQualifiedNameSet.elm: -------------------------------------------------------------------------------- 1 | module Code.FullyQualifiedNameSet exposing 2 | ( FQNSet 3 | , empty 4 | , insert 5 | , member 6 | , remove 7 | , size 8 | , toList 9 | , toggle 10 | ) 11 | 12 | import Code.FullyQualifiedName as FQN exposing (FQN) 13 | import Set exposing (Set) 14 | import Set.Extra 15 | 16 | 17 | 18 | -- FQNSet - A Set wrapper for FQN 19 | 20 | 21 | type FQNSet 22 | = FQNSet (Set String) 23 | 24 | 25 | size : FQNSet -> Int 26 | size (FQNSet set) = 27 | Set.size set 28 | 29 | 30 | empty : FQNSet 31 | empty = 32 | FQNSet Set.empty 33 | 34 | 35 | insert : FQN -> FQNSet -> FQNSet 36 | insert fqn (FQNSet set) = 37 | FQNSet (Set.insert (FQN.toString fqn) set) 38 | 39 | 40 | remove : FQN -> FQNSet -> FQNSet 41 | remove fqn (FQNSet set) = 42 | FQNSet (Set.remove (FQN.toString fqn) set) 43 | 44 | 45 | member : FQN -> FQNSet -> Bool 46 | member fqn (FQNSet set) = 47 | Set.member (FQN.toString fqn) set 48 | 49 | 50 | toList : FQNSet -> List FQN 51 | toList (FQNSet set) = 52 | set 53 | |> Set.toList 54 | |> List.map FQN.fromString 55 | 56 | 57 | toggle : FQN -> FQNSet -> FQNSet 58 | toggle fqn (FQNSet set) = 59 | FQNSet (Set.Extra.toggle (FQN.toString fqn) set) 60 | -------------------------------------------------------------------------------- /src/Code/Hash.elm: -------------------------------------------------------------------------------- 1 | module Code.Hash exposing 2 | ( Hash 3 | , decode 4 | , equals 5 | , fromString 6 | , fromUrlString 7 | , isAbilityConstructorHash 8 | , isAssumedBuiltin 9 | , isDataConstructorHash 10 | , isRawHash 11 | , prefix 12 | , stripHashPrefix 13 | , toShortString 14 | , toString 15 | , toUrlString 16 | , unsafeFromString 17 | , urlParser 18 | , urlPrefix 19 | ) 20 | 21 | import Json.Decode as Decode 22 | import Lib.Util as Util 23 | import Regex 24 | import Url.Parser 25 | 26 | 27 | type Hash 28 | = Hash String 29 | 30 | 31 | equals : Hash -> Hash -> Bool 32 | equals (Hash a) (Hash b) = 33 | a == b 34 | 35 | 36 | {-| TODO: Should this remove the prefix? 37 | -} 38 | toString : Hash -> String 39 | toString (Hash raw) = 40 | raw 41 | 42 | 43 | {-| Converts a Hash to a shortened (9 characters including the `#` character) 44 | of the raw hash value. 45 | 46 | Example: 47 | 48 | - Hash "#cv93ajol371idlcd47do5g3nmj7...4s829ofv57mi19pls3l630" -> "#cv93ajol" 49 | 50 | Note that it does not shorten hashes that are assumed to be for builtins: 51 | 52 | - Hash "##Debug.watch" -> "##Debug.watch" 53 | - Hash "##IO.socketSend.impl" -> "##IO.SocketSend.impl" 54 | 55 | -} 56 | toShortString : Hash -> String 57 | toShortString h = 58 | let 59 | shorten s = 60 | if isAssumedBuiltin h then 61 | s 62 | 63 | else 64 | String.left 9 s 65 | in 66 | h |> toString |> shorten 67 | 68 | 69 | {-| Assuming a Hash string, it strips _any number_ of prefixes at the beginning 70 | of the string 71 | 72 | Examples: 73 | 74 | - "#abc123def456" -> "abc123def456" 75 | - "##IO.socketSend.impl" -> "IO.socketSend.impl" 76 | 77 | This is often useful when rendering next to a Hash icon. 78 | 79 | -} 80 | stripHashPrefix : String -> String 81 | stripHashPrefix s = 82 | let 83 | re = 84 | Maybe.withDefault Regex.never (Regex.fromString "^(#+)") 85 | in 86 | Regex.replace re (\_ -> "") s 87 | 88 | 89 | fromString : String -> Maybe Hash 90 | fromString raw = 91 | if String.startsWith prefix raw then 92 | Just (Hash raw) 93 | 94 | else 95 | Nothing 96 | 97 | 98 | {-| !! Don't use this function outside of testing. It provides no guarantees 99 | for the correctness of the Hash. 100 | -} 101 | unsafeFromString : String -> Hash 102 | unsafeFromString raw = 103 | Hash raw 104 | 105 | 106 | isRawHash : String -> Bool 107 | isRawHash str = 108 | String.startsWith prefix str || String.startsWith urlPrefix str 109 | 110 | 111 | {-| Checking a hash starts weith 2 `#` characters is a weak heuristic for 112 | builtins, but sometimes useful. 113 | 114 | - Hash "##IO.socketSend.impl" -> True 115 | - Hash "##Debug.watch" -> True 116 | - Hash "#abc123def456" -> False 117 | 118 | -} 119 | isAssumedBuiltin : Hash -> Bool 120 | isAssumedBuiltin hash_ = 121 | hash_ |> toString |> String.startsWith "##" 122 | 123 | 124 | fromUrlString : String -> Maybe Hash 125 | fromUrlString str = 126 | if String.startsWith urlPrefix str then 127 | str 128 | |> String.replace urlPrefix prefix 129 | |> fromString 130 | 131 | else 132 | Nothing 133 | 134 | 135 | toUrlString : Hash -> String 136 | toUrlString hash = 137 | hash 138 | |> toString 139 | |> String.replace prefix urlPrefix 140 | 141 | 142 | prefix : String 143 | prefix = 144 | "#" 145 | 146 | 147 | urlPrefix : String 148 | urlPrefix = 149 | "@" 150 | 151 | 152 | 153 | -- HELPERS 154 | 155 | 156 | isDataConstructorHash : Hash -> Bool 157 | isDataConstructorHash hash = 158 | let 159 | dataConstructorSuffix = 160 | Maybe.withDefault Regex.never (Regex.fromString "#d(\\d+)$") 161 | in 162 | hash |> toString |> Regex.contains dataConstructorSuffix 163 | 164 | 165 | isAbilityConstructorHash : Hash -> Bool 166 | isAbilityConstructorHash hash = 167 | let 168 | abilityConstructorSuffix = 169 | Maybe.withDefault Regex.never (Regex.fromString "#a(\\d+)$") 170 | in 171 | hash |> toString |> Regex.contains abilityConstructorSuffix 172 | 173 | 174 | 175 | -- PARSERS 176 | 177 | 178 | urlParser : Url.Parser.Parser (Hash -> a) a 179 | urlParser = 180 | Url.Parser.custom "HASH" fromUrlString 181 | 182 | 183 | decode : Decode.Decoder Hash 184 | decode = 185 | Decode.map fromString Decode.string |> Decode.andThen (Util.decodeFailInvalid "Invalid Hash") 186 | -------------------------------------------------------------------------------- /src/Code/Hashvatar.elm: -------------------------------------------------------------------------------- 1 | module Code.Hashvatar exposing (..) 2 | 3 | import Code.Hash as Hash exposing (Hash) 4 | import Code.Hashvatar.HexGrid as HexGrid 5 | import Html exposing (Html, div) 6 | import Html.Attributes exposing (class) 7 | import List.Extra as ListE 8 | import String.Extra exposing (break) 9 | import UI.Color as Color exposing (Color) 10 | 11 | 12 | empty : Html msg 13 | empty = 14 | view_ HexGrid.empty 15 | 16 | 17 | view : Hash -> Html msg 18 | view hash = 19 | let 20 | raw = 21 | hash |> Hash.toString |> String.replace "#" "" 22 | 23 | numSlots = 24 | 5 25 | 26 | partLength = 27 | String.length raw // numSlots 28 | 29 | parts = 30 | break partLength raw 31 | 32 | toCharCodeSum str = 33 | str 34 | |> String.toList 35 | |> List.foldl (\c acc -> acc + Char.toCode c) 0 36 | 37 | grid = 38 | parts 39 | |> List.map toCharCodeSum 40 | |> toGrid 41 | |> Maybe.withDefault HexGrid.empty 42 | in 43 | view_ grid 44 | 45 | 46 | view_ : HexGrid.HexGrid -> Html msg 47 | view_ grid = 48 | div [ class "hashvatar" ] [ HexGrid.view grid ] 49 | 50 | 51 | 52 | -- Helpers 53 | 54 | 55 | getIn : Int -> List Color -> Maybe Color 56 | getIn unmoddedIdx colors_ = 57 | ListE.getAt (modBy (List.length colors_) unmoddedIdx) colors_ 58 | 59 | 60 | toGrid : List Int -> Maybe HexGrid.HexGrid 61 | toGrid slots = 62 | let 63 | selectBg grid_ = 64 | let 65 | background = 66 | getIn grid_.background Color.darkNonGrays 67 | in 68 | Maybe.map 69 | (\bg -> 70 | { background = bg 71 | , tendrils = grid_.tendrils 72 | , cell1 = grid_.cell1 73 | , cell2 = grid_.cell2 74 | , cell3 = grid_.cell3 75 | } 76 | ) 77 | background 78 | 79 | selectTendrils grid_ = 80 | let 81 | tendrils = 82 | getIn grid_.tendrils (Color.inSameHue grid_.background) 83 | in 84 | Maybe.map 85 | (\tr -> 86 | { background = grid_.background 87 | , tendrils = tr 88 | , cell1 = grid_.cell1 89 | , cell2 = grid_.cell2 90 | , cell3 = grid_.cell3 91 | } 92 | ) 93 | tendrils 94 | 95 | selectCells grid_ = 96 | Maybe.map3 97 | (\cell1 cell2 cell3 -> 98 | { background = grid_.background 99 | , tendrils = grid_.tendrils 100 | , cell1 = cell1 101 | , cell2 = cell2 102 | , cell3 = cell3 103 | } 104 | ) 105 | (getIn grid_.cell1 (Color.harmonizesWith grid_.background)) 106 | (getIn grid_.cell2 (Color.harmonizesWith grid_.background)) 107 | (getIn grid_.cell3 (Color.harmonizesWith grid_.background)) 108 | in 109 | Maybe.map5 110 | (\background tendrils cell1 cell2 cell3 -> 111 | { background = background 112 | , tendrils = tendrils 113 | , cell1 = cell1 114 | , cell2 = cell2 115 | , cell3 = cell3 116 | } 117 | ) 118 | (ListE.getAt 0 slots) 119 | (ListE.getAt 1 slots) 120 | (ListE.getAt 2 slots) 121 | (ListE.getAt 3 slots) 122 | (ListE.getAt 4 slots) 123 | |> Maybe.andThen selectBg 124 | |> Maybe.andThen selectTendrils 125 | |> Maybe.andThen selectCells 126 | -------------------------------------------------------------------------------- /src/Code/Namespace.elm: -------------------------------------------------------------------------------- 1 | module Code.Namespace exposing (..) 2 | 3 | import Code.Definition.Readme as Readme exposing (Readme) 4 | import Code.FullyQualifiedName as FQN exposing (FQN) 5 | import Code.Hash as Hash exposing (Hash) 6 | import Json.Decode as Decode exposing (field, maybe) 7 | 8 | 9 | type Namespace a 10 | = Namespace FQN Hash a 11 | 12 | 13 | type alias NamespaceDetailsFields = 14 | { readme : Maybe Readme } 15 | 16 | 17 | type alias NamespaceDetails = 18 | Namespace NamespaceDetailsFields 19 | 20 | 21 | 22 | -- Helpers -------------------------------------------------------------------- 23 | 24 | 25 | fqn : Namespace a -> FQN 26 | fqn (Namespace fqn_ _ _) = 27 | fqn_ 28 | 29 | 30 | hash : Namespace a -> Hash 31 | hash (Namespace _ h _) = 32 | h 33 | 34 | 35 | readme : NamespaceDetails -> Maybe Readme 36 | readme (Namespace _ _ details) = 37 | details.readme 38 | 39 | 40 | 41 | -- Decode --------------------------------------------------------------------- 42 | 43 | 44 | decodeDetails : Decode.Decoder NamespaceDetails 45 | decodeDetails = 46 | let 47 | makeDetails fqn_ hash_ readme_ = 48 | Namespace fqn_ hash_ { readme = readme_ } 49 | in 50 | Decode.map3 makeDetails 51 | (field "fqn" FQN.decode) 52 | (field "hash" Hash.decode) 53 | (maybe (field "readme" Readme.decode)) 54 | -------------------------------------------------------------------------------- /src/Code/Namespace/NamespaceRef.elm: -------------------------------------------------------------------------------- 1 | module Code.Namespace.NamespaceRef exposing (NamespaceRef(..), toString) 2 | 3 | import Code.FullyQualifiedName as FQN exposing (FQN) 4 | import Code.Hash as Hash exposing (Hash) 5 | 6 | 7 | type NamespaceRef 8 | = HashRef Hash 9 | | NameRef FQN 10 | 11 | 12 | toString : NamespaceRef -> String 13 | toString id = 14 | case id of 15 | HashRef h -> 16 | Hash.toString h 17 | 18 | NameRef fqn -> 19 | FQN.toString fqn 20 | -------------------------------------------------------------------------------- /src/Code/Project.elm: -------------------------------------------------------------------------------- 1 | module Code.Project exposing (..) 2 | 3 | import Code.FullyQualifiedName as FQN exposing (FQN) 4 | import Code.Hash as Hash exposing (Hash) 5 | import Code.Hashvatar as Hashvatar 6 | import Html exposing (Html) 7 | import Html.Attributes exposing (class) 8 | import Json.Decode as Decode exposing (field, string) 9 | import UI.Click as Click 10 | 11 | 12 | type Owner 13 | = Owner String 14 | 15 | 16 | type alias Project a = 17 | { a | owner : Owner, name : FQN, hash : Hash } 18 | 19 | 20 | type alias ProjectListing = 21 | Project {} 22 | 23 | 24 | slug : Project a -> FQN 25 | slug project = 26 | FQN.cons (ownerToString project.owner) project.name 27 | 28 | 29 | slugString : Project a -> String 30 | slugString project = 31 | project |> slug |> FQN.toString 32 | 33 | 34 | ownerToString : Owner -> String 35 | ownerToString (Owner o) = 36 | o 37 | 38 | 39 | equals : Project a -> Project b -> Bool 40 | equals a b = 41 | Hash.equals a.hash b.hash 42 | 43 | 44 | 45 | -- View 46 | 47 | 48 | viewSlug : Project a -> Html msg 49 | viewSlug project = 50 | project |> slug |> FQN.view 51 | 52 | 53 | viewProjectListing : Click.Click msg -> Project a -> Html msg 54 | viewProjectListing click project = 55 | Click.view [ class "project-listing" ] 56 | [ Hashvatar.view project.hash 57 | , viewSlug project 58 | ] 59 | click 60 | 61 | 62 | 63 | -- Decode 64 | 65 | 66 | decodeListing : Decode.Decoder ProjectListing 67 | decodeListing = 68 | let 69 | mk owner name hash = 70 | { owner = owner, name = name, hash = hash } 71 | in 72 | Decode.map3 73 | mk 74 | (field "owner" (Decode.map Owner string)) 75 | (field "name" FQN.decode) 76 | (field "hash" Hash.decode) 77 | 78 | 79 | decodeListings : Decode.Decoder (List ProjectListing) 80 | decodeListings = 81 | Decode.list decodeListing 82 | -------------------------------------------------------------------------------- /src/Code/README.md: -------------------------------------------------------------------------------- 1 | Code 2 | ==== 3 | 4 | A Unison UI library for rendering Unison Code. 5 | -------------------------------------------------------------------------------- /src/Code/UrlParsers.elm: -------------------------------------------------------------------------------- 1 | {- 2 | 3 | UrlParsers 4 | ============= 5 | 6 | Various parsing helpers to grab structured data like FQNs and Hashes out of 7 | routes 8 | -} 9 | 10 | 11 | module Code.UrlParsers exposing (..) 12 | 13 | import Code.Definition.Reference exposing (Reference(..)) 14 | import Code.FullyQualifiedName as FQN exposing (FQN) 15 | import Code.Hash as Hash exposing (Hash) 16 | import Code.HashQualified exposing (HashQualified(..)) 17 | import Code.Perspective exposing (PerspectiveParams(..), RootPerspectiveParam(..)) 18 | import Parser exposing ((|.), (|=), Parser, backtrackable, keyword, succeed) 19 | 20 | 21 | 22 | -- Parsers -------------------------------------------------------------------- 23 | 24 | 25 | fqn : Parser FQN 26 | fqn = 27 | let 28 | segment = 29 | Parser.oneOf 30 | -- Special case ;. which is an escaped . (dot), since we also use 31 | -- ';' as the separator character between namespace FQNs and 32 | -- definition FQNs. (';' is not a valid character in FQNs and is 33 | -- safe as a separator/escape character). 34 | [ b (succeed "." |. s ";.") 35 | , b chompSegment 36 | ] 37 | 38 | chompSegment = 39 | Parser.getChompedString <| 40 | Parser.succeed () 41 | |. Parser.chompWhile FQN.isValidUrlSegmentChar 42 | in 43 | Parser.map FQN.fromUrlList 44 | (Parser.sequence 45 | { start = "" 46 | , separator = "/" 47 | , end = "" 48 | , spaces = Parser.spaces 49 | , item = segment 50 | , trailing = Parser.Forbidden 51 | } 52 | ) 53 | 54 | 55 | fqnEnd : Parser () 56 | fqnEnd = 57 | Parser.symbol ";" 58 | 59 | 60 | hash : Parser Hash 61 | hash = 62 | let 63 | handleMaybe mHash = 64 | case mHash of 65 | Just h -> 66 | Parser.succeed h 67 | 68 | Nothing -> 69 | Parser.problem "Invalid hash" 70 | in 71 | Parser.chompUntilEndOr "/" 72 | |> Parser.getChompedString 73 | |> Parser.map Hash.fromUrlString 74 | |> Parser.andThen handleMaybe 75 | 76 | 77 | hq : Parser HashQualified 78 | hq = 79 | Parser.oneOf 80 | [ b (succeed HashOnly |= hash) 81 | , b (succeed NameOnly |= fqn) 82 | ] 83 | 84 | 85 | reference : Parser Reference 86 | reference = 87 | Parser.oneOf 88 | [ b (succeed TermReference |. s "terms" |. slash |= hq) 89 | , b (succeed TypeReference |. s "types" |. slash |= hq) 90 | , b (succeed AbilityConstructorReference |. s "ability-constructors" |. slash |= hq) 91 | , b (succeed DataConstructorReference |. s "data-constructors" |. slash |= hq) 92 | ] 93 | 94 | 95 | codebaseRef : Parser RootPerspectiveParam 96 | codebaseRef = 97 | Parser.oneOf 98 | [ b (succeed Relative |. s "latest") 99 | , b (succeed Absolute |= hash) 100 | ] 101 | 102 | 103 | perspectiveParams : Parser PerspectiveParams 104 | perspectiveParams = 105 | Parser.oneOf 106 | [ b (succeed ByNamespace |= codebaseRef |. slash |. s "namespaces" |. slash |= fqn |. fqnEnd) 107 | , b (succeed ByNamespace |= codebaseRef |. slash |. s "namespaces" |. slash |= fqn) 108 | , b (succeed ByRoot |= codebaseRef) 109 | ] 110 | 111 | 112 | 113 | -- Helpers -------------------------------------------------------------------- 114 | 115 | 116 | slash : Parser () 117 | slash = 118 | Parser.symbol "/" 119 | 120 | 121 | b : Parser a -> Parser a 122 | b = 123 | backtrackable 124 | 125 | 126 | s : String -> Parser () 127 | s = 128 | keyword 129 | -------------------------------------------------------------------------------- /src/Code/Workspace/Zoom.elm: -------------------------------------------------------------------------------- 1 | module Code.Workspace.Zoom exposing (..) 2 | 3 | 4 | type Zoom 5 | = Far 6 | | Medium 7 | | Near 8 | 9 | 10 | cycle : Zoom -> Zoom 11 | cycle z = 12 | case z of 13 | Far -> 14 | Medium 15 | 16 | Medium -> 17 | Near 18 | 19 | Near -> 20 | Far 21 | 22 | 23 | cycleEdges : Zoom -> Zoom 24 | cycleEdges z = 25 | case z of 26 | Far -> 27 | Near 28 | 29 | Medium -> 30 | Far 31 | 32 | Near -> 33 | Far 34 | -------------------------------------------------------------------------------- /src/Lib/Color/Harmony.elm: -------------------------------------------------------------------------------- 1 | module Lib.Color.Harmony exposing (..) 2 | 3 | import Color exposing (Color) 4 | import List.Extra as ListE 5 | 6 | 7 | harmonizesWith : Color -> List Color 8 | harmonizesWith color = 9 | let 10 | complementary_ = 11 | complementary color 12 | 13 | ( analogousA, analogousB ) = 14 | analogous color 15 | 16 | ( triadicA, triadicB ) = 17 | triadic color 18 | 19 | ( splitA, splitB ) = 20 | splitComplementary color 21 | 22 | ( squareA, squareB, squareC ) = 23 | square color 24 | 25 | ( tetridicA, tetridicB, tetridicC ) = 26 | tetridic color 27 | 28 | harmonizesWith_ = 29 | [ complementary_ 30 | , analogousA 31 | , analogousB 32 | , triadicA 33 | , triadicB 34 | , splitA 35 | , splitB 36 | , squareA 37 | , squareB 38 | , squareC 39 | , tetridicA 40 | , tetridicB 41 | , tetridicC 42 | ] 43 | in 44 | ListE.uniqueBy Color.toCssString harmonizesWith_ 45 | 46 | 47 | {-| RGB Difference - alpha is disregarded 48 | -} 49 | difference : Color -> Color -> Float 50 | difference a b = 51 | let 52 | a_ = 53 | toRgb255 a 54 | 55 | b_ = 56 | toRgb255 b 57 | 58 | sum = 59 | toFloat (((a_.red - b_.red) ^ 2) + ((a_.blue - b_.blue) ^ 2) + ((a_.green - b_.green) ^ 2)) 60 | in 61 | sqrt sum 62 | 63 | 64 | toRgb255 : Color -> { red : Int, green : Int, blue : Int } 65 | toRgb255 c = 66 | let 67 | rgba = 68 | Color.toRgba c 69 | in 70 | { red = floor (rgba.red * 255) 71 | , green = floor (rgba.red * 255) 72 | , blue = floor (rgba.blue * 255) 73 | } 74 | 75 | 76 | {-| Opposites on the color wheel 77 | -} 78 | complementary : Color -> Color 79 | complementary color = 80 | hueAdd 180 color 81 | 82 | 83 | {-| Adjacent colors on the color wheel 84 | -} 85 | analogous : Color -> ( Color, Color ) 86 | analogous color = 87 | ( hueAdd 30 color 88 | , hueSubtract 30 color 89 | ) 90 | 91 | 92 | triadic : Color -> ( Color, Color ) 93 | triadic color = 94 | ( hueAdd 120 color 95 | , hueAdd 240 color 96 | ) 97 | 98 | 99 | splitComplementary : Color -> ( Color, Color ) 100 | splitComplementary color = 101 | ( hueAdd 150 color 102 | , hueAdd 210 color 103 | ) 104 | 105 | 106 | square : Color -> ( Color, Color, Color ) 107 | square color = 108 | ( hueAdd 90 color 109 | , hueAdd 180 color 110 | , hueAdd 270 color 111 | ) 112 | 113 | 114 | tetridic : Color -> ( Color, Color, Color ) 115 | tetridic color = 116 | ( hueAdd 60 color 117 | , hueAdd 180 color 118 | , hueAdd 240 color 119 | ) 120 | 121 | 122 | 123 | -- Internal Helpers 124 | 125 | 126 | hueAdd : Int -> Color -> Color 127 | hueAdd num color = 128 | let 129 | hsla = 130 | Color.toHsla color 131 | 132 | hue = 133 | hsla.hue 134 | |> toAngle 135 | |> (+) num 136 | |> wrap360 137 | |> toPt 138 | in 139 | Color.fromHsla { hsla | hue = hue } 140 | 141 | 142 | hueSubtract : Int -> Color -> Color 143 | hueSubtract num color = 144 | let 145 | hsla = 146 | Color.toHsla color 147 | 148 | hue = 149 | hsla.hue 150 | |> toAngle 151 | |> (-) num 152 | |> wrap360 153 | |> toPt 154 | in 155 | Color.fromHsla { hsla | hue = hue } 156 | 157 | 158 | toAngle : Float -> Int 159 | toAngle pt = 160 | let 161 | a = 162 | floor (pt * 360) 163 | in 164 | if a > 360 then 165 | 360 - (360 - a) 166 | 167 | else 168 | a 169 | 170 | 171 | toPt : Int -> Float 172 | toPt ang = 173 | toFloat ang / 360 174 | 175 | 176 | wrap360 : Int -> Int 177 | wrap360 h = 178 | let 179 | x = 180 | modBy 360 h 181 | in 182 | if x < 0 then 183 | x + 360 184 | 185 | else 186 | x 187 | -------------------------------------------------------------------------------- /src/Lib/HttpApi.elm: -------------------------------------------------------------------------------- 1 | module Lib.HttpApi exposing 2 | ( ApiBasePath(..) 3 | , ApiRequest 4 | , EndpointUrl(..) 5 | , perform 6 | , toRequest 7 | , toTask 8 | , toUrl 9 | ) 10 | 11 | import Http 12 | import Json.Decode as Decode 13 | import Task exposing (Task) 14 | import Url.Builder exposing (QueryParameter, absolute) 15 | 16 | 17 | 18 | {- 19 | 20 | Api 21 | === 22 | 23 | Various helpers and types to deal with constructing, building up, and passing 24 | around Api requests 25 | 26 | -} 27 | 28 | 29 | {-| An EndpointUrl represents a level above a Url String. It includes paths and 30 | query parameters in a structured way such that the structure can be built up 31 | over several steps. 32 | -} 33 | type EndpointUrl 34 | = EndpointUrl (List String) (List QueryParameter) 35 | 36 | 37 | {-| A base path to indicate the base of the URL that the HTTP API exists on 38 | ex: ApiBasePath ["api"] 39 | -} 40 | type ApiBasePath 41 | = ApiBasePath (List String) 42 | 43 | 44 | toUrl : ApiBasePath -> EndpointUrl -> String 45 | toUrl (ApiBasePath basePath) (EndpointUrl paths queryParams) = 46 | absolute (basePath ++ paths) queryParams 47 | 48 | 49 | 50 | -- REQUEST -------------------------------------------------------------------- 51 | 52 | 53 | type alias HttpResult a = 54 | Result Http.Error a 55 | 56 | 57 | {-| Combines an EndpointUrl with a Decoder and a HttpResult to Msg function. 58 | Required to perform a call to the API. 59 | -} 60 | type ApiRequest a msg 61 | = ApiRequest EndpointUrl (Decode.Decoder a) (HttpResult a -> msg) 62 | 63 | 64 | toRequest : Decode.Decoder a -> (HttpResult a -> msg) -> EndpointUrl -> ApiRequest a msg 65 | toRequest decoder toMsg endpoint = 66 | ApiRequest endpoint decoder toMsg 67 | 68 | 69 | perform : ApiBasePath -> ApiRequest a msg -> Cmd msg 70 | perform basePath (ApiRequest endpoint decoder toMsg) = 71 | Http.get 72 | { url = toUrl basePath endpoint 73 | , expect = Http.expectJson toMsg decoder 74 | } 75 | 76 | 77 | 78 | --- TASK ---------------------------------------------------------------------- 79 | 80 | 81 | {-| TODO Perhaps this API should be merged into ApiRequest fully?? | 82 | -} 83 | toTask : ApiBasePath -> Decode.Decoder a -> EndpointUrl -> Task Http.Error a 84 | toTask basePath decoder endpoint = 85 | Http.task 86 | { method = "GET" 87 | , headers = [] 88 | , url = toUrl basePath endpoint 89 | , body = Http.emptyBody 90 | , resolver = Http.stringResolver (httpJsonBodyResolver decoder) 91 | , timeout = Nothing 92 | } 93 | 94 | 95 | httpJsonBodyResolver : Decode.Decoder a -> Http.Response String -> HttpResult a 96 | httpJsonBodyResolver decoder resp = 97 | case resp of 98 | Http.GoodStatus_ _ s -> 99 | Decode.decodeString decoder s 100 | |> Result.mapError (Decode.errorToString >> Http.BadBody) 101 | 102 | Http.BadUrl_ s -> 103 | Err (Http.BadUrl s) 104 | 105 | Http.Timeout_ -> 106 | Err Http.Timeout 107 | 108 | Http.NetworkError_ -> 109 | Err Http.NetworkError 110 | 111 | Http.BadStatus_ m s -> 112 | Decode.decodeString decoder s 113 | -- just trying; if our decoder understands the response body, great 114 | |> Result.mapError (\_ -> Http.BadStatus m.statusCode) 115 | -------------------------------------------------------------------------------- /src/Lib/OperatingSystem.elm: -------------------------------------------------------------------------------- 1 | module Lib.OperatingSystem exposing (..) 2 | 3 | 4 | type OperatingSystem 5 | = MacOS 6 | | Windows 7 | | Linux 8 | | Android 9 | | IOS 10 | | Unknown 11 | 12 | 13 | fromString : String -> OperatingSystem 14 | fromString rawOs = 15 | case rawOs of 16 | "macOS" -> 17 | MacOS 18 | 19 | "iOS" -> 20 | IOS 21 | 22 | "Windows" -> 23 | Windows 24 | 25 | "Android" -> 26 | Android 27 | 28 | "Linux" -> 29 | Linux 30 | 31 | _ -> 32 | Unknown 33 | -------------------------------------------------------------------------------- /src/Lib/README.md: -------------------------------------------------------------------------------- 1 | Lib 2 | === 3 | 4 | Common utility types and helpers. Does not include any UI directly. 5 | -------------------------------------------------------------------------------- /src/Lib/SearchResults.elm: -------------------------------------------------------------------------------- 1 | module Lib.SearchResults exposing 2 | ( Matches 3 | , SearchResults(..) 4 | , empty 5 | , focus 6 | , focusOn 7 | , from 8 | , fromList 9 | , getAt 10 | , isEmpty 11 | , length 12 | , map 13 | , mapMatchesToList 14 | , mapToList 15 | , matchesToList 16 | , next 17 | , prev 18 | , toList 19 | , toMaybe 20 | ) 21 | 22 | import List.Extra as ListE 23 | import List.Zipper as Zipper exposing (Zipper) 24 | import Maybe.Extra exposing (unwrap) 25 | 26 | 27 | 28 | -- SEARCH RESULT 29 | 30 | 31 | type SearchResults a 32 | = Empty 33 | | SearchResults (Matches a) 34 | 35 | 36 | empty : SearchResults a 37 | empty = 38 | Empty 39 | 40 | 41 | isEmpty : SearchResults a -> Bool 42 | isEmpty results = 43 | case results of 44 | Empty -> 45 | True 46 | 47 | SearchResults _ -> 48 | False 49 | 50 | 51 | fromList : List a -> SearchResults a 52 | fromList data = 53 | unwrap Empty (Matches >> SearchResults) (Zipper.fromList data) 54 | 55 | 56 | from : List a -> a -> List a -> SearchResults a 57 | from before focus_ after = 58 | SearchResults (Matches (Zipper.from before focus_ after)) 59 | 60 | 61 | length : SearchResults a -> Int 62 | length results = 63 | case results of 64 | Empty -> 65 | 0 66 | 67 | SearchResults (Matches data) -> 68 | data 69 | |> Zipper.toList 70 | |> List.length 71 | 72 | 73 | getAt : Int -> SearchResults a -> Maybe a 74 | getAt index results = 75 | results |> toList |> ListE.getAt index 76 | 77 | 78 | map : (Matches a -> Matches a) -> SearchResults a -> SearchResults a 79 | map f results = 80 | case results of 81 | Empty -> 82 | Empty 83 | 84 | SearchResults matches -> 85 | SearchResults (f matches) 86 | 87 | 88 | mapToList : (a -> Bool -> b) -> SearchResults a -> List b 89 | mapToList f results = 90 | case results of 91 | Empty -> 92 | [] 93 | 94 | SearchResults matches -> 95 | mapMatchesToList f matches 96 | 97 | 98 | toMaybe : SearchResults a -> Maybe (Matches a) 99 | toMaybe results = 100 | case results of 101 | Empty -> 102 | Nothing 103 | 104 | SearchResults matches -> 105 | Just matches 106 | 107 | 108 | toList : SearchResults a -> List a 109 | toList results = 110 | case results of 111 | Empty -> 112 | [] 113 | 114 | SearchResults matches -> 115 | matchesToList matches 116 | 117 | 118 | next : SearchResults a -> SearchResults a 119 | next = 120 | map nextMatch 121 | 122 | 123 | prev : SearchResults a -> SearchResults a 124 | prev = 125 | map prevMatch 126 | 127 | 128 | focusOn : (a -> Bool) -> SearchResults a -> SearchResults a 129 | focusOn pred results = 130 | case results of 131 | Empty -> 132 | Empty 133 | 134 | SearchResults matches -> 135 | SearchResults (focusOnMatch pred matches) 136 | 137 | 138 | 139 | -- MATCHES 140 | 141 | 142 | type Matches a 143 | = Matches (Zipper a) 144 | 145 | 146 | nextMatch : Matches a -> Matches a 147 | nextMatch ((Matches data) as matches) = 148 | unwrap matches Matches (Zipper.next data) 149 | 150 | 151 | prevMatch : Matches a -> Matches a 152 | prevMatch ((Matches data) as matches) = 153 | unwrap matches Matches (Zipper.previous data) 154 | 155 | 156 | focus : Matches a -> a 157 | focus (Matches data) = 158 | Zipper.current data 159 | 160 | 161 | focusOnMatch : (a -> Bool) -> Matches a -> Matches a 162 | focusOnMatch pred ((Matches data) as matches) = 163 | unwrap matches Matches (Zipper.findFirst pred data) 164 | 165 | 166 | {-| TODO: Should this be List.Nonempty ? | 167 | -} 168 | matchesToList : Matches a -> List a 169 | matchesToList (Matches data) = 170 | Zipper.toList data 171 | 172 | 173 | mapMatchesToList : (a -> Bool -> b) -> Matches a -> List b 174 | mapMatchesToList f (Matches data) = 175 | let 176 | before = 177 | data 178 | |> Zipper.before 179 | |> List.map (\a -> f a False) 180 | 181 | focus_ = 182 | f (Zipper.current data) True 183 | 184 | after = 185 | data 186 | |> Zipper.after 187 | |> List.map (\a -> f a False) 188 | in 189 | before ++ (focus_ :: after) 190 | -------------------------------------------------------------------------------- /src/Lib/TreePath.elm: -------------------------------------------------------------------------------- 1 | {-- 2 | 3 | TreePath 4 | ======== 5 | 6 | Simple type for keeping tracking of a path down a tree of variants that 7 | could have lists as data. Definition.Doc is the main example of a tree 8 | structure that uses TreePath. 9 | 10 | --} 11 | 12 | 13 | module Lib.TreePath exposing (TreePath, TreePathItem(..), toString) 14 | 15 | 16 | type TreePathItem 17 | = VariantIndex Int 18 | | ListIndex Int 19 | 20 | 21 | type alias TreePath = 22 | List TreePathItem 23 | 24 | 25 | toString : TreePath -> String 26 | toString path = 27 | let 28 | pathItemToString item = 29 | case item of 30 | VariantIndex i -> 31 | "VariantIndex#" ++ String.fromInt i 32 | 33 | ListIndex i -> 34 | "ListIndex#" ++ String.fromInt i 35 | in 36 | path |> List.map pathItemToString |> String.join "." 37 | -------------------------------------------------------------------------------- /src/Lib/Util.elm: -------------------------------------------------------------------------------- 1 | module Lib.Util exposing (..) 2 | 3 | import Http 4 | import Json.Decode as Decode 5 | import List.Nonempty as NEL 6 | import Process 7 | import Task 8 | 9 | 10 | 11 | -- Various utility functions and helpers 12 | 13 | 14 | delayMsg : Float -> msg -> Cmd msg 15 | delayMsg delay msg = 16 | Task.perform (\_ -> msg) (Process.sleep delay) 17 | 18 | 19 | decodeNonEmptyList : Decode.Decoder a -> Decode.Decoder (NEL.Nonempty a) 20 | decodeNonEmptyList = 21 | Decode.list 22 | >> Decode.andThen 23 | (\list -> 24 | case NEL.fromList list of 25 | Just nel -> 26 | Decode.succeed nel 27 | 28 | Nothing -> 29 | Decode.fail "Decoded an empty list" 30 | ) 31 | 32 | 33 | decodeFailInvalid : String -> Maybe a -> Decode.Decoder a 34 | decodeFailInvalid failMessage m = 35 | case m of 36 | Nothing -> 37 | Decode.fail failMessage 38 | 39 | Just a -> 40 | Decode.succeed a 41 | 42 | 43 | decodeTag : Decode.Decoder String 44 | decodeTag = 45 | Decode.field "tag" Decode.string 46 | 47 | 48 | httpErrorToString : Http.Error -> String 49 | httpErrorToString err = 50 | case err of 51 | Http.Timeout -> 52 | "Timeout exceeded" 53 | 54 | Http.NetworkError -> 55 | "Network error" 56 | 57 | Http.BadStatus status -> 58 | "Bad status: " ++ String.fromInt status 59 | 60 | Http.BadBody text -> 61 | "Unexpected response from api: " ++ text 62 | 63 | Http.BadUrl url -> 64 | "Malformed url: " ++ url 65 | -------------------------------------------------------------------------------- /src/Lib/detectOs.js: -------------------------------------------------------------------------------- 1 | const macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"]; 2 | const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"]; 3 | const iosPlatforms = ["iPhone", "iPad", "iPod"]; 4 | 5 | export default function detectOs(nav) { 6 | const { userAgent, platform } = nav; 7 | 8 | if (macosPlatforms.includes(platform)) { 9 | return "macOS"; 10 | } else if (iosPlatforms.includes(platform)) { 11 | return "iOS"; 12 | } else if (windowsPlatforms.includes(platform)) { 13 | return "Windows"; 14 | } else if (/Android/.test(userAgent)) { 15 | return "Android"; 16 | } else if (/Linux/.test(platform)) { 17 | return "Linux"; 18 | } else { 19 | return "Unknown"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Lib/preventDefaultGlobalKeyboardEvents.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Firefox has binds for Ctrl+k, Cmd+k, and "/" We want to use those to open 3 | * the Finder. 4 | * 5 | * Unfortunately we can't do this in Elm, since Browser.Events doesn't support 6 | * preventDefault, so we're duplicating the shortcuts and calling 7 | * preventDefault in JS. The Elm handler picks up the event later on. 8 | * 9 | * TODO: This is a bit brittle and relies on the Elm handler being added after 10 | * this one. There might be a better solution build with ports for this. 11 | */ 12 | 13 | function preventDefaultGlobalKeyboardEvents() { 14 | window.addEventListener("keydown", (ev) => { 15 | if ( 16 | ev.key === "/" || 17 | (ev.metaKey && ev.key == "k") || 18 | (ev.ctrlKey && ev.key == "k") 19 | ) { 20 | ev.preventDefault(); 21 | } 22 | }); 23 | } 24 | 25 | export default preventDefaultGlobalKeyboardEvents; 26 | -------------------------------------------------------------------------------- /src/UI.elm: -------------------------------------------------------------------------------- 1 | module UI exposing (..) 2 | 3 | import Html exposing (Attribute, Html, code, div, hr, pre, span, strong, text) 4 | import Html.Attributes exposing (class) 5 | import Html.Events exposing (onClick) 6 | import UI.Icon as Icon 7 | 8 | 9 | codeBlock : List (Attribute msg) -> Html msg -> Html msg 10 | codeBlock attrs code_ = 11 | pre attrs [ code [] [ code_ ] ] 12 | 13 | 14 | bold : String -> Html msg 15 | bold text_ = 16 | strong [] [ text text_ ] 17 | 18 | 19 | inlineCode : List (Attribute msg) -> Html msg -> Html msg 20 | inlineCode attrs code_ = 21 | code (class "inline-code" :: attrs) [ code_ ] 22 | 23 | 24 | nothing : Html msg 25 | nothing = 26 | text "" 27 | 28 | 29 | badge : Html msg -> Html msg 30 | badge content = 31 | span [ class "badge" ] [ content ] 32 | 33 | 34 | optionBadge : msg -> Html msg -> Html msg 35 | optionBadge removeMsg content = 36 | span [ class "option-badge", onClick removeMsg ] [ Icon.view Icon.x, content ] 37 | 38 | 39 | subtle : String -> Html msg 40 | subtle label = 41 | span [ class "subtle" ] [ text label ] 42 | 43 | 44 | loadingPlaceholder : Html msg 45 | loadingPlaceholder = 46 | div [ class "loading-placeholder" ] [] 47 | 48 | 49 | loadingPlaceholderRow : Html msg 50 | loadingPlaceholderRow = 51 | div [ class "loading-placeholder-row" ] 52 | [ div [ class "loading-placeholder" ] [] 53 | ] 54 | 55 | 56 | errorMessage : String -> Html msg 57 | errorMessage message = 58 | div [ class "error-message" ] [ text message ] 59 | 60 | 61 | emptyStateMessage : String -> Html msg 62 | emptyStateMessage message = 63 | div [ class "empty-state" ] [ text message ] 64 | 65 | 66 | divider : Html msg 67 | divider = 68 | hr [ class "divider" ] [] 69 | 70 | 71 | charWidth : Int -> String 72 | charWidth numChars = 73 | String.fromInt numChars ++ "ch" 74 | -------------------------------------------------------------------------------- /src/UI/AppHeader.elm: -------------------------------------------------------------------------------- 1 | module UI.AppHeader exposing (..) 2 | 3 | import Html exposing (Html, a, header, section) 4 | import Html.Attributes exposing (class, id) 5 | import Html.Events exposing (onClick) 6 | import UI 7 | import UI.Banner as Banner exposing (Banner) 8 | import UI.Button as Button exposing (Button) 9 | import UI.Click as Click exposing (Click) 10 | import UI.Icon as Icon 11 | 12 | 13 | type AppTitle msg 14 | = AppTitle (Click msg) (Html msg) 15 | 16 | 17 | type alias MenuToggle msg = 18 | { onClick : msg } 19 | 20 | 21 | type alias AppHeader msg = 22 | { menuToggle : Maybe msg 23 | , appTitle : AppTitle msg 24 | , banner : Maybe (Banner msg) 25 | , rightButton : Maybe (Button msg) 26 | } 27 | 28 | 29 | appHeader : AppTitle msg -> AppHeader msg 30 | appHeader appTitle = 31 | { menuToggle = Nothing 32 | , appTitle = appTitle 33 | , banner = Nothing 34 | , rightButton = Nothing 35 | } 36 | 37 | 38 | view : AppHeader msg -> Html msg 39 | view appHeader_ = 40 | let 41 | menuToggle = 42 | case appHeader_.menuToggle of 43 | Nothing -> 44 | UI.nothing 45 | 46 | Just toggle -> 47 | a 48 | [ class "menu-toggle", onClick toggle ] 49 | [ Icon.view Icon.list ] 50 | 51 | banner = 52 | case appHeader_.banner of 53 | Nothing -> 54 | UI.nothing 55 | 56 | Just banner_ -> 57 | Banner.view banner_ 58 | 59 | rightButton = 60 | appHeader_.rightButton 61 | |> Maybe.map Button.small 62 | |> Maybe.map Button.view 63 | |> Maybe.withDefault UI.nothing 64 | in 65 | view_ 66 | [ menuToggle 67 | , viewAppTitle appHeader_.appTitle 68 | , section [ class "right" ] [ banner, rightButton ] 69 | ] 70 | 71 | 72 | viewAppTitle : AppTitle msg -> Html msg 73 | viewAppTitle (AppTitle click content) = 74 | Click.view [ class "app-title" ] [ content ] click 75 | 76 | 77 | view_ : List (Html msg) -> Html msg 78 | view_ content = 79 | header [ id "app-header" ] content 80 | -------------------------------------------------------------------------------- /src/UI/Banner.elm: -------------------------------------------------------------------------------- 1 | module UI.Banner exposing (..) 2 | 3 | import Html exposing (Html, span, text) 4 | import Html.Attributes exposing (class) 5 | import UI.Click as Click exposing (Click) 6 | 7 | 8 | type Banner msg 9 | = Info { content : String } 10 | | Promotion { promotionId : String, content : String, ctaClick : Click msg, ctaLabel : String } 11 | 12 | 13 | info : String -> Banner msg 14 | info content = 15 | Info { content = content } 16 | 17 | 18 | promotion : String -> String -> Click msg -> String -> Banner msg 19 | promotion promotionId content ctaClick ctaLabel = 20 | Promotion 21 | { promotionId = promotionId 22 | , content = content 23 | , ctaClick = ctaClick 24 | , ctaLabel = ctaLabel 25 | } 26 | 27 | 28 | view : Banner msg -> Html msg 29 | view banner_ = 30 | case banner_ of 31 | Info i -> 32 | span [ class "banner", class "info" ] [ text i.content ] 33 | 34 | Promotion p -> 35 | Click.view 36 | [ class "banner" 37 | , class "promotion" 38 | , class p.promotionId 39 | ] 40 | [ text p.content 41 | , span [ class "banner-cta" ] [ text p.ctaLabel ] 42 | ] 43 | p.ctaClick 44 | -------------------------------------------------------------------------------- /src/UI/Card.elm: -------------------------------------------------------------------------------- 1 | module UI.Card exposing (..) 2 | 3 | import Html exposing (Html, div, h3, text) 4 | import Html.Attributes exposing (class) 5 | 6 | 7 | type CardType 8 | = Contained 9 | | Uncontained 10 | 11 | 12 | type alias Card msg = 13 | { type_ : CardType 14 | , title : Maybe String 15 | , items : List (Html msg) 16 | } 17 | 18 | 19 | card : List (Html msg) -> Card msg 20 | card items = 21 | { type_ = Uncontained, title = Nothing, items = items } 22 | 23 | 24 | titled : String -> List (Html msg) -> Card msg 25 | titled title items = 26 | { type_ = Uncontained, title = Just title, items = items } 27 | 28 | 29 | withType : CardType -> Card msg -> Card msg 30 | withType type_ card_ = 31 | { card_ | type_ = type_ } 32 | 33 | 34 | asContained : Card msg -> Card msg 35 | asContained card_ = 36 | { card_ | type_ = Contained } 37 | 38 | 39 | withTitle : String -> Card msg -> Card msg 40 | withTitle title card_ = 41 | { card_ | title = Just title } 42 | 43 | 44 | withItems : List (Html msg) -> Card msg -> Card msg 45 | withItems items card_ = 46 | { card_ | items = items } 47 | 48 | 49 | withItem : Html msg -> Card msg -> Card msg 50 | withItem item card_ = 51 | { card_ | items = card_.items ++ [ item ] } 52 | 53 | 54 | view : Card msg -> Html msg 55 | view card_ = 56 | let 57 | items = 58 | case card_.title of 59 | Just t -> 60 | h3 [ class "card-title" ] [ text t ] :: card_.items 61 | 62 | Nothing -> 63 | card_.items 64 | 65 | typeClass = 66 | case card_.type_ of 67 | Contained -> 68 | "contained" 69 | 70 | Uncontained -> 71 | "uncontained" 72 | in 73 | div [ class "card", class typeClass ] items 74 | -------------------------------------------------------------------------------- /src/UI/Click.elm: -------------------------------------------------------------------------------- 1 | module UI.Click exposing (..) 2 | 3 | import Html exposing (Attribute, Html, a) 4 | import Html.Attributes exposing (href, rel, target) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | type Click msg 9 | = ExternalHref String 10 | | Href String -- Internal route 11 | | OnClick msg 12 | | Disabled 13 | 14 | 15 | attrs : Click msg -> List (Attribute msg) 16 | attrs click = 17 | case click of 18 | ExternalHref href_ -> 19 | [ href href_, rel "noopener", target "_blank" ] 20 | 21 | Href href_ -> 22 | [ href href_ ] 23 | 24 | OnClick msg -> 25 | [ onClick msg ] 26 | 27 | Disabled -> 28 | [] 29 | 30 | 31 | view : List (Attribute msg) -> List (Html msg) -> Click msg -> Html msg 32 | view attrs_ content click = 33 | a (attrs_ ++ attrs click) content 34 | -------------------------------------------------------------------------------- /src/UI/CopyField.elm: -------------------------------------------------------------------------------- 1 | module UI.CopyField exposing (..) 2 | 3 | import Html exposing (Html, button, div, input, node, text) 4 | import Html.Attributes exposing (attribute, class, readonly, type_, value) 5 | import UI 6 | import UI.Icon as Icon 7 | 8 | 9 | type alias CopyField msg = 10 | { prefix : Maybe String 11 | , toCopy : String 12 | , onCopy : String -> msg 13 | } 14 | 15 | 16 | copyField : (String -> msg) -> String -> CopyField msg 17 | copyField onCopy toCopy = 18 | { prefix = Nothing, toCopy = toCopy, onCopy = onCopy } 19 | 20 | 21 | withPrefix : String -> CopyField msg -> CopyField msg 22 | withPrefix prefix field = 23 | { field | prefix = Just prefix } 24 | 25 | 26 | withToCopy : String -> CopyField msg -> CopyField msg 27 | withToCopy toCopy field = 28 | { field | toCopy = toCopy } 29 | 30 | 31 | view : CopyField msg -> Html msg 32 | view field = 33 | let 34 | prefix = 35 | field.prefix 36 | |> Maybe.map (\p -> div [ class "copy-field-prefix" ] [ text p ]) 37 | |> Maybe.withDefault UI.nothing 38 | in 39 | div [ class "copy-field" ] 40 | [ div [ class "copy-field-field" ] 41 | [ prefix 42 | , div 43 | [ class "copy-field-input" ] 44 | [ input 45 | [ type_ "text" 46 | , class "copy-field-to-copy" 47 | , value field.toCopy 48 | , readonly True 49 | ] 50 | [] 51 | ] 52 | ] 53 | , copyButton field.toCopy 54 | ] 55 | 56 | 57 | 58 | -- HELPERS -------------------------------------------------------------------- 59 | 60 | 61 | {-| We're not using UI.Button here since a click handler is added from 62 | the webcomponent in JS land. 63 | -} 64 | copyButton : String -> Html msg 65 | copyButton toCopy = 66 | node "copy-on-click" 67 | [ attribute "text" toCopy ] 68 | [ button [ class "button contained default" ] [ Icon.view Icon.clipboard ] 69 | ] 70 | -------------------------------------------------------------------------------- /src/UI/CopyOnClick.js: -------------------------------------------------------------------------------- 1 | // 2 | // clickable content 3 | // 4 | // 5 | // Use from Elm with an Icon: 6 | // node "copy-on-click" [ ] [ UI.Icon.view UI.Icon.clipboard ] 7 | class CopyOnClick extends HTMLElement { 8 | constructor() { 9 | super(); 10 | } 11 | 12 | connectedCallback() { 13 | this.addEventListener("click", () => { 14 | const text = this.getAttribute("text"); 15 | 16 | // writeText returns a promise with success/failure that we should 17 | // probably do something with... 18 | navigator.clipboard.writeText(text); 19 | }); 20 | } 21 | 22 | static get observedAttributes() { 23 | return ["text"]; 24 | } 25 | } 26 | 27 | customElements.define("copy-on-click", CopyOnClick); 28 | -------------------------------------------------------------------------------- /src/UI/FoldToggle.elm: -------------------------------------------------------------------------------- 1 | module UI.FoldToggle exposing (..) 2 | 3 | import Html exposing (Html, div) 4 | import Html.Attributes exposing (classList) 5 | import Html.Events exposing (onClick) 6 | import UI.Icon as Icon 7 | 8 | 9 | type Position 10 | = Opened 11 | | Closed 12 | 13 | 14 | type Toggleable msg 15 | = OnToggle msg 16 | | Disabled 17 | 18 | 19 | type alias FoldToggle msg = 20 | { toggleable : Toggleable msg 21 | , position : Position 22 | } 23 | 24 | 25 | 26 | -- CREATE 27 | 28 | 29 | foldToggle : msg -> FoldToggle msg 30 | foldToggle toggleMsg = 31 | FoldToggle (OnToggle toggleMsg) Closed 32 | 33 | 34 | disabled : FoldToggle msg 35 | disabled = 36 | FoldToggle Disabled Closed 37 | 38 | 39 | 40 | -- MODIFY 41 | 42 | 43 | withPosition : Position -> FoldToggle msg -> FoldToggle msg 44 | withPosition position toggle = 45 | { toggle | position = position } 46 | 47 | 48 | withToggleable : Toggleable msg -> FoldToggle msg -> FoldToggle msg 49 | withToggleable toggleable toggle = 50 | { toggle | toggleable = toggleable } 51 | 52 | 53 | isOpen : Bool -> FoldToggle msg -> FoldToggle msg 54 | isOpen isOpen_ toggle = 55 | let 56 | position = 57 | if isOpen_ then 58 | Opened 59 | 60 | else 61 | Closed 62 | in 63 | { toggle | position = position } 64 | 65 | 66 | isClosed : Bool -> FoldToggle msg -> FoldToggle msg 67 | isClosed isClosed_ toggle = 68 | isOpen (not isClosed_) toggle 69 | 70 | 71 | open : FoldToggle msg -> FoldToggle msg 72 | open toggle = 73 | withPosition Opened toggle 74 | 75 | 76 | close : FoldToggle msg -> FoldToggle msg 77 | close toggle = 78 | withPosition Closed toggle 79 | 80 | 81 | isDisabled : Bool -> FoldToggle msg -> FoldToggle msg 82 | isDisabled isDisabled_ toggle = 83 | if isDisabled_ then 84 | withToggleable Disabled toggle 85 | 86 | else 87 | toggle 88 | 89 | 90 | 91 | -- VIEW 92 | 93 | 94 | view : FoldToggle msg -> Html msg 95 | view toggle = 96 | let 97 | isOpen_ = 98 | toggle.position == Opened 99 | 100 | ( onClickAttrs, isDisabled_ ) = 101 | case toggle.toggleable of 102 | OnToggle msg -> 103 | ( [ onClick msg ], False ) 104 | 105 | Disabled -> 106 | ( [], True ) 107 | in 108 | div 109 | (classList 110 | [ ( "fold-toggle", True ) 111 | , ( "folded-open", isOpen_ ) 112 | , ( "disabled", isDisabled_ ) 113 | ] 114 | :: onClickAttrs 115 | ) 116 | -- Caret orientation for folded/unfolded is rotated 117 | -- by CSS such that it can be animated 118 | [ Icon.view Icon.caretRight ] 119 | -------------------------------------------------------------------------------- /src/UI/Modal.elm: -------------------------------------------------------------------------------- 1 | module UI.Modal exposing 2 | ( Content(..) 3 | , Modal 4 | , modal 5 | , view 6 | , withAttributes 7 | , withHeader 8 | ) 9 | 10 | import Html exposing (Attribute, Html, a, div, h2, header, section, text) 11 | import Html.Attributes exposing (class, id, tabindex) 12 | import Html.Events exposing (on, onClick) 13 | import Json.Decode as Decode 14 | import UI 15 | import UI.Icon as Icon 16 | 17 | 18 | type Content msg 19 | = Content (Html msg) 20 | | CustomContent (Html msg) 21 | 22 | 23 | type alias Modal msg = 24 | { id : String 25 | , closeMsg : msg 26 | , attributes : List (Attribute msg) 27 | , header : Maybe (Html msg) 28 | , content : Content msg 29 | } 30 | 31 | 32 | modal : String -> msg -> Content msg -> Modal msg 33 | modal id closeMsg content = 34 | { id = id 35 | , closeMsg = closeMsg 36 | , attributes = [] 37 | , header = Nothing 38 | , content = content 39 | } 40 | 41 | 42 | withHeader : String -> Modal msg -> Modal msg 43 | withHeader title modal_ = 44 | { modal_ | header = Just (text title) } 45 | 46 | 47 | withAttributes : List (Attribute msg) -> Modal msg -> Modal msg 48 | withAttributes attrs modal_ = 49 | { modal_ | attributes = modal_.attributes ++ attrs } 50 | 51 | 52 | view : Modal msg -> Html msg 53 | view modal_ = 54 | let 55 | header_ = 56 | modal_.header 57 | |> Maybe.map 58 | (\title -> 59 | header [ class "modal-header " ] 60 | [ h2 [] [ title ] 61 | , a [ class "close-modal", onClick modal_.closeMsg ] 62 | [ Icon.view Icon.x ] 63 | ] 64 | ) 65 | |> Maybe.withDefault UI.nothing 66 | 67 | content = 68 | case modal_.content of 69 | Content c -> 70 | section [ class "modal-content" ] [ c ] 71 | 72 | CustomContent c -> 73 | c 74 | in 75 | view_ modal_.closeMsg (id modal_.id :: modal_.attributes) [ header_, content ] 76 | 77 | 78 | 79 | -- INTERNALS 80 | 81 | 82 | view_ : msg -> List (Attribute msg) -> List (Html msg) -> Html msg 83 | view_ closeMsg attrs content = 84 | div [ id overlayId, on "click" (decodeOverlayClick closeMsg) ] 85 | [ div (tabindex 0 :: class "modal" :: attrs) content 86 | ] 87 | 88 | 89 | overlayId : String 90 | overlayId = 91 | "modal-overlay" 92 | 93 | 94 | decodeOverlayClick : msg -> Decode.Decoder msg 95 | decodeOverlayClick closeMsg = 96 | Decode.at [ "target", "id" ] Decode.string 97 | |> Decode.andThen 98 | (\c -> 99 | if String.contains overlayId c then 100 | Decode.succeed closeMsg 101 | 102 | else 103 | Decode.fail "ignoring" 104 | ) 105 | -------------------------------------------------------------------------------- /src/UI/PageLayout.elm: -------------------------------------------------------------------------------- 1 | module UI.PageLayout exposing (..) 2 | 3 | import Html exposing (Html, div, header, section) 4 | import Html.Attributes exposing (class, classList) 5 | import UI.Sidebar as Sidebar 6 | 7 | 8 | type PageHero msg 9 | = PageHero (Html msg) 10 | 11 | 12 | type PageContent msg 13 | = PageContent (List (Html msg)) 14 | 15 | 16 | type PageLayout msg 17 | = HeroLayout 18 | { hero : PageHero msg 19 | , content : 20 | PageContent msg 21 | } 22 | | SidebarLayout 23 | { sidebar : List (Html msg) 24 | , sidebarToggled : Bool 25 | , content : PageContent msg 26 | } 27 | | FullLayout { content : PageContent msg } 28 | 29 | 30 | 31 | -- VIEW 32 | 33 | 34 | viewHero : PageHero msg -> Html msg 35 | viewHero (PageHero content) = 36 | header [ class "page-hero" ] [ content ] 37 | 38 | 39 | viewContent : PageContent msg -> Html msg 40 | viewContent (PageContent content) = 41 | section [ class "page-content" ] content 42 | 43 | 44 | view : PageLayout msg -> Html msg 45 | view page = 46 | case page of 47 | HeroLayout { hero, content } -> 48 | div [ class "page hero-layout" ] 49 | [ viewHero hero 50 | , viewContent content 51 | ] 52 | 53 | SidebarLayout { sidebar, sidebarToggled, content } -> 54 | div 55 | [ class "page sidebar-layout" 56 | , classList [ ( "sidebar-toggled", sidebarToggled ) ] 57 | ] 58 | [ Sidebar.view sidebar 59 | , viewContent content 60 | ] 61 | 62 | FullLayout { content } -> 63 | div [ class "page full-layout" ] 64 | [ viewContent content 65 | ] 66 | -------------------------------------------------------------------------------- /src/UI/README.md: -------------------------------------------------------------------------------- 1 | Unison Design System 2 | ==================== 3 | 4 | Fonts, Colors, and Components. 5 | -------------------------------------------------------------------------------- /src/UI/Sidebar.elm: -------------------------------------------------------------------------------- 1 | module UI.Sidebar exposing (..) 2 | 3 | import Html exposing (Attribute, Html, a, aside, div, h3, label, text) 4 | import Html.Attributes exposing (class, id) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | header : List (Html msg) -> Html msg 9 | header content = 10 | Html.header [ class "sidebar-header" ] content 11 | 12 | 13 | headerItem : List (Attribute msg) -> List (Html msg) -> Html msg 14 | headerItem attrs content = 15 | div (attrs ++ [ class "sidebar-header-item" ]) content 16 | 17 | 18 | section : String -> List (Html msg) -> Html msg 19 | section label content = 20 | Html.section [ class "sidebar-section" ] 21 | (sectionTitle label :: content) 22 | 23 | 24 | sectionTitle : String -> Html msg 25 | sectionTitle label = 26 | h3 [ class "sidebar-section-title" ] [ text label ] 27 | 28 | 29 | item : msg -> String -> Html msg 30 | item clickMsg label_ = 31 | a 32 | [ class "sidebar-item" 33 | , onClick clickMsg 34 | ] 35 | [ label [] [ text label_ ] 36 | ] 37 | 38 | 39 | view : List (Html msg) -> Html msg 40 | view content = 41 | aside [ id "main-sidebar" ] content 42 | -------------------------------------------------------------------------------- /src/UI/Toolbar.elm: -------------------------------------------------------------------------------- 1 | module UI.Toolbar exposing (..) 2 | 3 | import Html exposing (Html, header) 4 | import Html.Attributes exposing (class) 5 | 6 | 7 | type alias Toolbar msg = 8 | { content : Html msg 9 | } 10 | 11 | 12 | toolbar : Html msg -> Toolbar msg 13 | toolbar = 14 | Toolbar 15 | 16 | 17 | view : Toolbar msg -> Html msg 18 | view { content } = 19 | header [ class "toolbar" ] [ content ] 20 | -------------------------------------------------------------------------------- /src/UI/Tooltip.elm: -------------------------------------------------------------------------------- 1 | module UI.Tooltip exposing 2 | ( Arrow(..) 3 | , Content(..) 4 | , MenuItem 5 | , Position(..) 6 | , Tooltip 7 | , menu 8 | , textMenu 9 | , tooltip 10 | , view 11 | , withArrow 12 | , withPosition 13 | ) 14 | 15 | import Html exposing (Html, div, text) 16 | import Html.Attributes exposing (class) 17 | import UI 18 | import UI.Click as Click exposing (Click) 19 | import UI.Icon as Icon exposing (Icon) 20 | 21 | 22 | type alias Tooltip msg = 23 | { arrow : Arrow 24 | , trigger : Html msg 25 | , content : Content msg 26 | , position : Position 27 | } 28 | 29 | 30 | type Arrow 31 | = None 32 | | Start 33 | | Middle 34 | | End 35 | 36 | 37 | {-| Position relative to trigger 38 | -} 39 | type Position 40 | = Above 41 | | Below 42 | | LeftOf 43 | | RightOf 44 | 45 | 46 | type alias MenuItem msg = 47 | { icon : Maybe (Icon msg), label : String, click : Click msg } 48 | 49 | 50 | type Content msg 51 | = Text String 52 | | Rich (Html msg) 53 | | Menu (List (MenuItem msg)) 54 | 55 | 56 | menu : List ( Icon msg, String, Click msg ) -> Content msg 57 | menu items = 58 | items 59 | |> List.map (\( i, l, c ) -> MenuItem (Just i) l c) 60 | |> Menu 61 | 62 | 63 | textMenu : List ( String, Click msg ) -> Content msg 64 | textMenu items = 65 | items 66 | |> List.map (\( l, c ) -> MenuItem Nothing l c) 67 | |> Menu 68 | 69 | 70 | tooltip : Html msg -> Content msg -> Tooltip msg 71 | tooltip trigger content = 72 | { arrow = Middle 73 | , trigger = trigger 74 | , content = content 75 | , position = Below 76 | } 77 | 78 | 79 | withArrow : Arrow -> Tooltip msg -> Tooltip msg 80 | withArrow arrow tooltip_ = 81 | { tooltip_ | arrow = arrow } 82 | 83 | 84 | withPosition : Position -> Tooltip msg -> Tooltip msg 85 | withPosition pos tooltip_ = 86 | { tooltip_ | position = pos } 87 | 88 | 89 | view : Tooltip msg -> Html msg 90 | view { arrow, content, trigger, position } = 91 | let 92 | viewMenuItem item = 93 | let 94 | iconHtml = 95 | case item.icon of 96 | Just icon -> 97 | Icon.view icon 98 | 99 | Nothing -> 100 | UI.nothing 101 | in 102 | Click.view [ class "tooltip-menu-item" ] [ iconHtml, text item.label ] item.click 103 | 104 | content_ = 105 | case content of 106 | Text t -> 107 | text t 108 | 109 | Rich html -> 110 | html 111 | 112 | Menu items -> 113 | div [ class "tooltip-menu-items" ] (List.map viewMenuItem items) 114 | 115 | tooltip_ = 116 | -- The tooltip includes a small bridge (made with padding) above 117 | -- the bubble to allow the user to hover into the tooltip and click 118 | -- links etc. 119 | div 120 | [ class "tooltip" 121 | , class (positionToClass position) 122 | , class 123 | (arrowToClass arrow) 124 | , class (contentToClass content) 125 | ] 126 | [ div 127 | [ class "tooltip-bubble" ] 128 | [ content_ ] 129 | ] 130 | in 131 | div [ class "tooltip-trigger" ] [ tooltip_, trigger ] 132 | 133 | 134 | 135 | -- INTERNAL 136 | 137 | 138 | contentToClass : Content msg -> String 139 | contentToClass content = 140 | case content of 141 | Text _ -> 142 | "content-text" 143 | 144 | Rich _ -> 145 | "content-rich" 146 | 147 | Menu _ -> 148 | "content-menu" 149 | 150 | 151 | positionToClass : Position -> String 152 | positionToClass pos = 153 | case pos of 154 | Above -> 155 | "above" 156 | 157 | Below -> 158 | "below" 159 | 160 | RightOf -> 161 | "right-of" 162 | 163 | LeftOf -> 164 | "left-of" 165 | 166 | 167 | arrowToClass : Arrow -> String 168 | arrowToClass arrow = 169 | case arrow of 170 | None -> 171 | "arrow-none" 172 | 173 | Start -> 174 | "arrow-start" 175 | 176 | Middle -> 177 | "arrow-middle" 178 | 179 | End -> 180 | "arrow-end" 181 | -------------------------------------------------------------------------------- /src/UnisonLocal.elm: -------------------------------------------------------------------------------- 1 | module UnisonLocal exposing (..) 2 | 3 | import Browser 4 | import UnisonLocal.App as App 5 | import UnisonLocal.Env exposing (Flags) 6 | import UnisonLocal.PreApp as PreApp 7 | 8 | 9 | main : Program Flags PreApp.Model PreApp.Msg 10 | main = 11 | Browser.application 12 | { init = PreApp.init 13 | , update = PreApp.update 14 | , view = PreApp.view 15 | , subscriptions = PreApp.subscriptions 16 | , onUrlRequest = App.LinkClicked >> PreApp.AppMsg 17 | , onUrlChange = App.UrlChanged >> PreApp.AppMsg 18 | } 19 | -------------------------------------------------------------------------------- /src/UnisonLocal/Api.elm: -------------------------------------------------------------------------------- 1 | module UnisonLocal.Api exposing 2 | ( codebaseApiEndpointToEndpointUrl 3 | , codebaseHash 4 | , namespace 5 | ) 6 | 7 | import Code.CodebaseApi as CodebaseApi 8 | import Code.Definition.Reference as Reference 9 | import Code.FullyQualifiedName as FQN exposing (FQN) 10 | import Code.Hash as Hash exposing (Hash) 11 | import Code.Namespace.NamespaceRef as NamespaceRef 12 | import Code.Perspective as Perspective exposing (Perspective(..)) 13 | import Code.Syntax as Syntax 14 | import Lib.HttpApi exposing (EndpointUrl(..)) 15 | import Regex 16 | import Url.Builder exposing (QueryParameter, int, string) 17 | 18 | 19 | codebaseHash : EndpointUrl 20 | codebaseHash = 21 | EndpointUrl [ "list" ] [ string "namespace" "." ] 22 | 23 | 24 | namespace : Perspective -> FQN -> EndpointUrl 25 | namespace perspective fqn = 26 | let 27 | queryParams = 28 | [ rootBranch (Perspective.rootHash perspective) ] 29 | in 30 | EndpointUrl [ "namespaces", FQN.toString fqn ] queryParams 31 | 32 | 33 | codebaseApiEndpointToEndpointUrl : CodebaseApi.CodebaseEndpoint -> EndpointUrl 34 | codebaseApiEndpointToEndpointUrl cbEndpoint = 35 | case cbEndpoint of 36 | CodebaseApi.Find { perspective, withinFqn, limit, sourceWidth, query } -> 37 | let 38 | params = 39 | case withinFqn of 40 | Just fqn -> 41 | [ rootBranch (Perspective.rootHash perspective), relativeTo fqn ] 42 | 43 | Nothing -> 44 | perspectiveToQueryParams perspective 45 | 46 | width = 47 | case sourceWidth of 48 | Syntax.Width w -> 49 | w 50 | in 51 | EndpointUrl 52 | [ "find" ] 53 | ([ int "limit" limit 54 | , int "renderWidth" width 55 | , string "query" query 56 | ] 57 | ++ params 58 | ) 59 | 60 | CodebaseApi.Browse { perspective, ref } -> 61 | let 62 | namespace_ = 63 | ref |> Maybe.map NamespaceRef.toString |> Maybe.withDefault "." 64 | in 65 | EndpointUrl [ "list" ] (string "namespace" namespace_ :: perspectiveToQueryParams perspective) 66 | 67 | CodebaseApi.Definition { perspective, ref } -> 68 | let 69 | re = 70 | Maybe.withDefault Regex.never (Regex.fromString "#[d|a|](\\d+)$") 71 | 72 | stripConstructorPositionFromHash = 73 | Regex.replace re (always "") 74 | in 75 | [ Reference.toApiUrlString ref ] 76 | |> List.map stripConstructorPositionFromHash 77 | |> List.map (string "names") 78 | |> (\names -> EndpointUrl [ "getDefinition" ] (names ++ perspectiveToQueryParams perspective)) 79 | 80 | 81 | 82 | -- QUERY PARAMS --------------------------------------------------------------- 83 | 84 | 85 | perspectiveToQueryParams : Perspective -> List QueryParameter 86 | perspectiveToQueryParams perspective = 87 | case perspective of 88 | Root h -> 89 | [ rootBranch h ] 90 | 91 | Namespace d -> 92 | [ rootBranch d.rootHash, relativeTo d.fqn ] 93 | 94 | 95 | rootBranch : Hash -> QueryParameter 96 | rootBranch hash = 97 | string "rootBranch" (hash |> Hash.toString) 98 | 99 | 100 | relativeTo : FQN -> QueryParameter 101 | relativeTo fqn = 102 | string "relativeTo" (fqn |> FQN.toString) 103 | -------------------------------------------------------------------------------- /src/UnisonLocal/Env.elm: -------------------------------------------------------------------------------- 1 | module UnisonLocal.Env exposing (..) 2 | 3 | import Browser.Navigation as Nav 4 | import Code.CodebaseApi as CodebaseApi 5 | import Code.Config 6 | import Code.Perspective exposing (Perspective) 7 | import Lib.HttpApi exposing (ApiBasePath(..)) 8 | import Lib.OperatingSystem as OS exposing (OperatingSystem) 9 | 10 | 11 | type alias Env = 12 | { operatingSystem : OperatingSystem 13 | , basePath : String 14 | , apiBasePath : ApiBasePath 15 | , navKey : Nav.Key 16 | , perspective : Perspective 17 | } 18 | 19 | 20 | type alias Flags = 21 | { operatingSystem : String 22 | , basePath : String 23 | , apiBasePath : List String 24 | } 25 | 26 | 27 | init : Flags -> Nav.Key -> Perspective -> Env 28 | init flags navKey perspective = 29 | { operatingSystem = OS.fromString flags.operatingSystem 30 | , basePath = flags.basePath 31 | , apiBasePath = ApiBasePath flags.apiBasePath 32 | , navKey = navKey 33 | , perspective = perspective 34 | } 35 | 36 | 37 | toCodeConfig : CodebaseApi.ToApiEndpointUrl -> Env -> Code.Config.Config 38 | toCodeConfig toApiEndpointUrl env = 39 | { operatingSystem = env.operatingSystem 40 | , perspective = env.perspective 41 | , toApiEndpointUrl = toApiEndpointUrl 42 | , apiBasePath = env.apiBasePath 43 | } 44 | -------------------------------------------------------------------------------- /src/UnisonLocal/PreApp.elm: -------------------------------------------------------------------------------- 1 | module UnisonLocal.PreApp exposing (..) 2 | 3 | import Browser 4 | import Browser.Navigation as Nav 5 | import Code.Perspective as Perspective exposing (Perspective, PerspectiveParams) 6 | import Html 7 | import Http 8 | import Lib.HttpApi as HttpApi exposing (ApiBasePath(..), ApiRequest) 9 | import UnisonLocal.Api as LocalApi 10 | import UnisonLocal.App as App 11 | import UnisonLocal.Env as Env exposing (Flags) 12 | import UnisonLocal.Route as Route exposing (Route) 13 | import Url exposing (Url) 14 | 15 | 16 | type Model 17 | = Initializing PreEnv 18 | | InitializationError PreEnv Http.Error 19 | | Initialized App.Model 20 | 21 | 22 | type alias PreEnv = 23 | { flags : Flags 24 | , route : Route 25 | , navKey : Nav.Key 26 | , perspectiveParams : PerspectiveParams 27 | } 28 | 29 | 30 | init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) 31 | init flags url navKey = 32 | let 33 | route = 34 | Route.fromUrl flags.basePath url 35 | 36 | preEnv = 37 | { flags = flags 38 | , route = route 39 | , navKey = navKey 40 | , perspectiveParams = Route.perspectiveParams route 41 | } 42 | 43 | perspectiveToAppInit perspective = 44 | let 45 | env = 46 | Env.init preEnv.flags preEnv.navKey perspective 47 | 48 | ( app, cmd ) = 49 | App.init env preEnv.route 50 | in 51 | ( Initialized app, Cmd.map AppMsg cmd ) 52 | 53 | fetchPerspective_ = 54 | ( Initializing preEnv, HttpApi.perform (ApiBasePath flags.apiBasePath) (fetchPerspective preEnv) ) 55 | in 56 | -- If we have a codebase hash we can construct a full perspective, 57 | -- otherwise we have to fetch the hash before being able to start up the 58 | -- app 59 | preEnv.perspectiveParams 60 | |> Perspective.fromParams 61 | |> Maybe.map perspectiveToAppInit 62 | |> Maybe.withDefault fetchPerspective_ 63 | 64 | 65 | fetchPerspective : PreEnv -> ApiRequest Perspective Msg 66 | fetchPerspective preEnv = 67 | LocalApi.codebaseHash |> HttpApi.toRequest (Perspective.decode preEnv.perspectiveParams) (FetchPerspectiveFinished preEnv) 68 | 69 | 70 | type Msg 71 | = FetchPerspectiveFinished PreEnv (Result Http.Error Perspective) 72 | | AppMsg App.Msg 73 | 74 | 75 | update : Msg -> Model -> ( Model, Cmd Msg ) 76 | update msg model = 77 | case msg of 78 | FetchPerspectiveFinished preEnv result -> 79 | case result of 80 | Ok perspective -> 81 | let 82 | env = 83 | Env.init preEnv.flags preEnv.navKey perspective 84 | 85 | newRoute = 86 | perspective 87 | |> Perspective.toParams 88 | |> Route.updatePerspectiveParams preEnv.route 89 | 90 | ( app, cmd ) = 91 | App.init env newRoute 92 | in 93 | ( Initialized app, Cmd.map AppMsg cmd ) 94 | 95 | Err err -> 96 | ( InitializationError preEnv err, Cmd.none ) 97 | 98 | AppMsg appMsg -> 99 | case model of 100 | Initialized a -> 101 | let 102 | ( app, cmd ) = 103 | App.update appMsg a 104 | in 105 | ( Initialized app, Cmd.map AppMsg cmd ) 106 | 107 | _ -> 108 | ( model, Cmd.none ) 109 | 110 | 111 | subscriptions : Model -> Sub Msg 112 | subscriptions model = 113 | case model of 114 | Initialized app -> 115 | Sub.map AppMsg (App.subscriptions app) 116 | 117 | _ -> 118 | Sub.none 119 | 120 | 121 | view : Model -> Browser.Document Msg 122 | view model = 123 | case model of 124 | Initializing _ -> 125 | { title = "Loading.." 126 | , body = [ App.viewAppLoading ] 127 | } 128 | 129 | InitializationError _ error -> 130 | { title = "Application Error" 131 | , body = [ App.viewAppError error ] 132 | } 133 | 134 | Initialized appModel -> 135 | let 136 | app = 137 | App.view appModel 138 | in 139 | { title = app.title 140 | , body = List.map (Html.map AppMsg) app.body 141 | } 142 | -------------------------------------------------------------------------------- /src/UnisonShare.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare exposing (..) 2 | 3 | import Browser 4 | import UnisonShare.App as App 5 | import UnisonShare.Env exposing (Flags) 6 | import UnisonShare.PreApp as PreApp 7 | 8 | 9 | main : Program Flags PreApp.Model PreApp.Msg 10 | main = 11 | Browser.application 12 | { init = PreApp.init 13 | , update = PreApp.update 14 | , view = PreApp.view 15 | , subscriptions = PreApp.subscriptions 16 | , onUrlRequest = App.LinkClicked >> PreApp.AppMsg 17 | , onUrlChange = App.UrlChanged >> PreApp.AppMsg 18 | } 19 | -------------------------------------------------------------------------------- /src/UnisonShare/Api.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.Api exposing 2 | ( catalog 3 | , codebaseApiEndpointToEndpointUrl 4 | , codebaseHash 5 | , namespace 6 | , projects 7 | ) 8 | 9 | import Code.CodebaseApi as CodebaseApi 10 | import Code.Definition.Reference as Reference 11 | import Code.FullyQualifiedName as FQN exposing (FQN) 12 | import Code.Hash as Hash exposing (Hash) 13 | import Code.Namespace.NamespaceRef as NamespaceRef 14 | import Code.Perspective as Perspective exposing (Perspective(..)) 15 | import Code.Syntax as Syntax 16 | import Lib.HttpApi exposing (EndpointUrl(..)) 17 | import Regex 18 | import Url.Builder exposing (QueryParameter, int, string) 19 | 20 | 21 | codebaseHash : EndpointUrl 22 | codebaseHash = 23 | EndpointUrl [ "list" ] [ string "namespace" "." ] 24 | 25 | 26 | namespace : Perspective -> FQN -> EndpointUrl 27 | namespace perspective fqn = 28 | let 29 | queryParams = 30 | [ rootBranch (Perspective.rootHash perspective) ] 31 | in 32 | EndpointUrl [ "namespaces", FQN.toString fqn ] queryParams 33 | 34 | 35 | projects : Maybe String -> EndpointUrl 36 | projects owner = 37 | let 38 | queryParams = 39 | case owner of 40 | Just owner_ -> 41 | [ string "owner" owner_ ] 42 | 43 | Nothing -> 44 | [] 45 | in 46 | EndpointUrl [ "projects" ] queryParams 47 | 48 | 49 | catalog : EndpointUrl 50 | catalog = 51 | [ "_catalog" ] 52 | |> List.map (string "names") 53 | |> (\names -> EndpointUrl [ "getDefinition" ] names) 54 | 55 | 56 | codebaseApiEndpointToEndpointUrl : CodebaseApi.CodebaseEndpoint -> EndpointUrl 57 | codebaseApiEndpointToEndpointUrl cbEndpoint = 58 | case cbEndpoint of 59 | CodebaseApi.Find { perspective, withinFqn, limit, sourceWidth, query } -> 60 | let 61 | params = 62 | case withinFqn of 63 | Just fqn -> 64 | [ rootBranch (Perspective.rootHash perspective), relativeTo fqn ] 65 | 66 | Nothing -> 67 | perspectiveToQueryParams perspective 68 | 69 | width = 70 | case sourceWidth of 71 | Syntax.Width w -> 72 | w 73 | in 74 | EndpointUrl 75 | [ "find" ] 76 | ([ int "limit" limit 77 | , int "renderWidth" width 78 | , string "query" query 79 | ] 80 | ++ params 81 | ) 82 | 83 | CodebaseApi.Browse { perspective, ref } -> 84 | let 85 | namespace_ = 86 | ref |> Maybe.map NamespaceRef.toString |> Maybe.withDefault "." 87 | in 88 | EndpointUrl [ "list" ] (string "namespace" namespace_ :: perspectiveToQueryParams perspective) 89 | 90 | CodebaseApi.Definition { perspective, ref } -> 91 | let 92 | re = 93 | Maybe.withDefault Regex.never (Regex.fromString "#[d|a|](\\d+)$") 94 | 95 | stripConstructorPositionFromHash = 96 | Regex.replace re (always "") 97 | in 98 | [ Reference.toApiUrlString ref ] 99 | |> List.map stripConstructorPositionFromHash 100 | |> List.map (string "names") 101 | |> (\names -> EndpointUrl [ "getDefinition" ] (names ++ perspectiveToQueryParams perspective)) 102 | 103 | 104 | 105 | -- QUERY PARAMS --------------------------------------------------------------- 106 | 107 | 108 | perspectiveToQueryParams : Perspective -> List QueryParameter 109 | perspectiveToQueryParams perspective = 110 | case perspective of 111 | Root h -> 112 | [ rootBranch h ] 113 | 114 | Namespace d -> 115 | [ rootBranch d.rootHash, relativeTo d.fqn ] 116 | 117 | 118 | rootBranch : Hash -> QueryParameter 119 | rootBranch hash = 120 | string "rootBranch" (hash |> Hash.toString) 121 | 122 | 123 | relativeTo : FQN -> QueryParameter 124 | relativeTo fqn = 125 | string "relativeTo" (fqn |> FQN.toString) 126 | -------------------------------------------------------------------------------- /src/UnisonShare/Catalog.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.Catalog exposing (..) 2 | 3 | import Code.FullyQualifiedName as FQN 4 | import Code.Project as Project exposing (ProjectListing) 5 | import Dict 6 | import Json.Decode as Decode 7 | import OrderedDict exposing (OrderedDict) 8 | import UnisonShare.Catalog.CatalogMask as CatalogMask exposing (CatalogMask) 9 | 10 | 11 | type Catalog 12 | = Catalog (OrderedDict String (List ProjectListing)) 13 | 14 | 15 | 16 | -- CREATE 17 | 18 | 19 | {-| Create the empty Catalog 20 | -} 21 | empty : Catalog 22 | empty = 23 | Catalog OrderedDict.empty 24 | 25 | 26 | {-| Create a Catalog using a mask and listings. The mask is used to get the 27 | categories and to ensure the category sort order is preserved 28 | -} 29 | catalog : CatalogMask -> List ProjectListing -> Catalog 30 | catalog mask projectListings_ = 31 | let 32 | group project acc = 33 | let 34 | projectName = 35 | project |> Project.slug |> FQN.toString 36 | 37 | categoryName = 38 | CatalogMask.categoryOf projectName mask 39 | 40 | set old = 41 | case old of 42 | Just ps -> 43 | Just (ps ++ [ project ]) 44 | 45 | Nothing -> 46 | Just [ project ] 47 | in 48 | case categoryName of 49 | Just c -> 50 | Dict.update c set acc 51 | 52 | Nothing -> 53 | acc 54 | 55 | grouped = 56 | List.foldl group Dict.empty projectListings_ 57 | 58 | sortedCategories category acc = 59 | case Dict.get category grouped of 60 | Just ps -> 61 | insert category ps acc 62 | 63 | Nothing -> 64 | acc 65 | in 66 | List.foldl sortedCategories empty (CatalogMask.categories mask) 67 | 68 | 69 | {-| Insert a category and projects within into a Catalog 70 | -} 71 | insert : String -> List ProjectListing -> Catalog -> Catalog 72 | insert categoryName projectListings_ (Catalog dict) = 73 | Catalog (OrderedDict.insert categoryName projectListings_ dict) 74 | 75 | 76 | {-| Create a Catalog given a list of projects grouped by category 77 | -} 78 | fromList : List ( String, List ProjectListing ) -> Catalog 79 | fromList items = 80 | items 81 | |> OrderedDict.fromList 82 | |> Catalog 83 | 84 | 85 | 86 | -- HELPERS 87 | 88 | 89 | isEmpty : Catalog -> Bool 90 | isEmpty (Catalog dict) = 91 | OrderedDict.isEmpty dict 92 | 93 | 94 | {-| Extract all categories from a Catalog 95 | -} 96 | categories : Catalog -> List String 97 | categories (Catalog dict) = 98 | OrderedDict.keys dict 99 | 100 | 101 | {-| Extract all project listings from a Catalog 102 | -} 103 | projectListings : Catalog -> List ProjectListing 104 | projectListings (Catalog dict) = 105 | List.concat (OrderedDict.values dict) 106 | 107 | 108 | {-| Convert a Catalog to a list of project listings grouped by category 109 | -} 110 | toList : Catalog -> List ( String, List ProjectListing ) 111 | toList (Catalog dict) = 112 | OrderedDict.toList dict 113 | 114 | 115 | 116 | -- DECODE 117 | 118 | 119 | decodeCatalogMask : Decode.Decoder CatalogMask 120 | decodeCatalogMask = 121 | CatalogMask.decode 122 | -------------------------------------------------------------------------------- /src/UnisonShare/Catalog/CatalogMask.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.Catalog.CatalogMask exposing 2 | ( CatalogMask 3 | , categories 4 | , categoryOf 5 | , decode 6 | , empty 7 | , fromDoc 8 | , fromList 9 | , isEmpty 10 | , projectNames 11 | , toList 12 | ) 13 | 14 | import Code.Definition.Doc as Doc exposing (Doc) 15 | import Json.Decode as Decode exposing (field, index) 16 | import List.Extra as ListE 17 | import OrderedDict exposing (OrderedDict) 18 | 19 | 20 | {-| CatalogMask is used to create the Catalog and map ProjectListings to their 21 | categories. 22 | 23 | Indexed by project to category "unison.http" -> "Web & Networking" 24 | 25 | -} 26 | type CatalogMask 27 | = CatalogMask (OrderedDict String String) 28 | 29 | 30 | 31 | -- Create 32 | 33 | 34 | empty : CatalogMask 35 | empty = 36 | CatalogMask OrderedDict.empty 37 | 38 | 39 | fromList : List ( String, String ) -> CatalogMask 40 | fromList categories_ = 41 | CatalogMask (OrderedDict.fromList categories_) 42 | 43 | 44 | {-| For right now, a CatalogMask is fetched as a Doc from the server and as 45 | such we can parse that into a CatalogMask if it has the right shape (if not, 46 | its an empty mask) 47 | -} 48 | fromDoc : Doc -> CatalogMask 49 | fromDoc doc = 50 | let 51 | category_ d = 52 | case d of 53 | Doc.Section category content -> 54 | case content of 55 | [ Doc.BulletedList projects ] -> 56 | List.map (\p -> ( Doc.toString "" p, Doc.toString " " category )) projects 57 | 58 | _ -> 59 | [] 60 | 61 | _ -> 62 | [] 63 | 64 | categories_ = 65 | case doc of 66 | Doc.UntitledSection ds -> 67 | List.concatMap category_ ds 68 | 69 | _ -> 70 | [] 71 | in 72 | fromList categories_ 73 | 74 | 75 | 76 | -- Helpers 77 | 78 | 79 | isEmpty : CatalogMask -> Bool 80 | isEmpty (CatalogMask mask) = 81 | OrderedDict.isEmpty mask 82 | 83 | 84 | categoryOf : String -> CatalogMask -> Maybe String 85 | categoryOf projectName (CatalogMask mask) = 86 | OrderedDict.get projectName mask 87 | 88 | 89 | categories : CatalogMask -> List String 90 | categories (CatalogMask mask) = 91 | OrderedDict.values mask |> ListE.unique 92 | 93 | 94 | projectNames : CatalogMask -> List String 95 | projectNames (CatalogMask mask) = 96 | OrderedDict.keys mask 97 | 98 | 99 | toList : CatalogMask -> List ( String, String ) 100 | toList (CatalogMask mask) = 101 | OrderedDict.toList mask 102 | 103 | 104 | 105 | -- Decode 106 | 107 | 108 | decode : Decode.Decoder CatalogMask 109 | decode = 110 | Decode.map fromDoc decodeCatalogDoc 111 | 112 | 113 | decodeCatalogDoc : Decode.Decoder Doc 114 | decodeCatalogDoc = 115 | let 116 | decodeDoc : Decode.Decoder Doc 117 | decodeDoc = 118 | field "termDocs" (index 0 (index 2 Doc.decode)) 119 | 120 | decodeTermDocs : Decode.Decoder (List Doc) 121 | decodeTermDocs = 122 | Decode.keyValuePairs decodeDoc |> Decode.map (List.map Tuple.second) 123 | 124 | decodeList : Decode.Decoder (List Doc) 125 | decodeList = 126 | field "termDefinitions" decodeTermDocs 127 | in 128 | Decode.map List.head decodeList 129 | |> Decode.andThen 130 | (Maybe.map Decode.succeed 131 | >> Maybe.withDefault (Decode.fail "Empty list") 132 | ) 133 | -------------------------------------------------------------------------------- /src/UnisonShare/Env.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.Env exposing (..) 2 | 3 | import Browser.Navigation as Nav 4 | import Code.CodebaseApi as CodebaseApi 5 | import Code.Config 6 | import Code.Perspective exposing (Perspective) 7 | import Lib.HttpApi exposing (ApiBasePath(..)) 8 | import Lib.OperatingSystem as OS exposing (OperatingSystem) 9 | 10 | 11 | type alias Env = 12 | { operatingSystem : OperatingSystem 13 | , basePath : String 14 | , apiBasePath : ApiBasePath 15 | , navKey : Nav.Key 16 | , perspective : Perspective 17 | } 18 | 19 | 20 | type alias Flags = 21 | { operatingSystem : String 22 | , basePath : String 23 | , apiBasePath : List String 24 | } 25 | 26 | 27 | init : Flags -> Nav.Key -> Perspective -> Env 28 | init flags navKey perspective = 29 | { operatingSystem = OS.fromString flags.operatingSystem 30 | , basePath = flags.basePath 31 | , apiBasePath = ApiBasePath flags.apiBasePath 32 | , navKey = navKey 33 | , perspective = perspective 34 | } 35 | 36 | 37 | toCodeConfig : CodebaseApi.ToApiEndpointUrl -> Env -> Code.Config.Config 38 | toCodeConfig toApiEndpointUrl env = 39 | { operatingSystem = env.operatingSystem 40 | , perspective = env.perspective 41 | , toApiEndpointUrl = toApiEndpointUrl 42 | , apiBasePath = env.apiBasePath 43 | } 44 | -------------------------------------------------------------------------------- /src/UnisonShare/Log.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.Log exposing (..) 2 | 3 | 4 | type LogEntryStatus 5 | = Success 6 | | Info 7 | | Error 8 | 9 | 10 | type alias LogEntry l = 11 | { l 12 | | status : LogEntryStatus 13 | , id : String 14 | , title : String 15 | , description : Maybe String 16 | , loggedAt : String 17 | } 18 | 19 | 20 | type alias Log a = 21 | List (LogEntry a) 22 | -------------------------------------------------------------------------------- /src/UnisonShare/PreApp.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.PreApp exposing (..) 2 | 3 | import Browser 4 | import Browser.Navigation as Nav 5 | import Code.Perspective as Perspective exposing (Perspective, PerspectiveParams) 6 | import Html 7 | import Http 8 | import Lib.HttpApi as HttpApi exposing (ApiBasePath(..), ApiRequest) 9 | import UnisonShare.Api as ShareApi 10 | import UnisonShare.App as App 11 | import UnisonShare.Env as Env exposing (Flags) 12 | import UnisonShare.Route as Route exposing (Route) 13 | import Url exposing (Url) 14 | 15 | 16 | type Model 17 | = Initializing PreEnv 18 | | InitializationError PreEnv Http.Error 19 | | Initialized App.Model 20 | 21 | 22 | type alias PreEnv = 23 | { flags : Flags 24 | , route : Route 25 | , navKey : Nav.Key 26 | , perspectiveParams : PerspectiveParams 27 | } 28 | 29 | 30 | init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) 31 | init flags url navKey = 32 | let 33 | route = 34 | Route.fromUrl flags.basePath url 35 | 36 | perspectiveParams = 37 | route 38 | |> Route.perspectiveParams 39 | |> Maybe.withDefault (Perspective.ByRoot Perspective.Relative) 40 | 41 | preEnv = 42 | { flags = flags 43 | , route = route 44 | , navKey = navKey 45 | , perspectiveParams = perspectiveParams 46 | } 47 | 48 | perspectiveToAppInit perspective = 49 | let 50 | env = 51 | Env.init preEnv.flags preEnv.navKey perspective 52 | 53 | ( app, cmd ) = 54 | App.init env preEnv.route 55 | in 56 | ( Initialized app, Cmd.map AppMsg cmd ) 57 | 58 | fetchPerspective_ = 59 | ( Initializing preEnv, HttpApi.perform (ApiBasePath flags.apiBasePath) (fetchPerspective preEnv) ) 60 | in 61 | -- If we have a codebase hash we can construct a full perspective, 62 | -- otherwise we have to fetch the hash before being able to start up the 63 | -- app 64 | preEnv.perspectiveParams 65 | |> Perspective.fromParams 66 | |> Maybe.map perspectiveToAppInit 67 | |> Maybe.withDefault fetchPerspective_ 68 | 69 | 70 | fetchPerspective : PreEnv -> ApiRequest Perspective Msg 71 | fetchPerspective preEnv = 72 | ShareApi.codebaseHash |> HttpApi.toRequest (Perspective.decode preEnv.perspectiveParams) (FetchPerspectiveFinished preEnv) 73 | 74 | 75 | type Msg 76 | = FetchPerspectiveFinished PreEnv (Result Http.Error Perspective) 77 | | AppMsg App.Msg 78 | 79 | 80 | update : Msg -> Model -> ( Model, Cmd Msg ) 81 | update msg model = 82 | case msg of 83 | FetchPerspectiveFinished preEnv result -> 84 | case result of 85 | Ok perspective -> 86 | let 87 | env = 88 | Env.init preEnv.flags preEnv.navKey perspective 89 | 90 | newRoute = 91 | perspective 92 | |> Perspective.toParams 93 | |> Route.updatePerspectiveParams preEnv.route 94 | 95 | ( app, cmd ) = 96 | App.init env newRoute 97 | in 98 | ( Initialized app, Cmd.map AppMsg cmd ) 99 | 100 | Err err -> 101 | ( InitializationError preEnv err, Cmd.none ) 102 | 103 | AppMsg appMsg -> 104 | case model of 105 | Initialized a -> 106 | let 107 | ( app, cmd ) = 108 | App.update appMsg a 109 | in 110 | ( Initialized app, Cmd.map AppMsg cmd ) 111 | 112 | _ -> 113 | ( model, Cmd.none ) 114 | 115 | 116 | subscriptions : Model -> Sub Msg 117 | subscriptions model = 118 | case model of 119 | Initialized app -> 120 | Sub.map AppMsg (App.subscriptions app) 121 | 122 | _ -> 123 | Sub.none 124 | 125 | 126 | view : Model -> Browser.Document Msg 127 | view model = 128 | case model of 129 | Initializing _ -> 130 | { title = "Loading.." 131 | , body = [ App.viewAppLoading ] 132 | } 133 | 134 | InitializationError _ error -> 135 | { title = "Application Error" 136 | , body = [ App.viewAppError error ] 137 | } 138 | 139 | Initialized appModel -> 140 | let 141 | app = 142 | App.view appModel 143 | in 144 | { title = app.title 145 | , body = List.map (Html.map AppMsg) app.body 146 | } 147 | -------------------------------------------------------------------------------- /src/UnisonShare/User.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.User exposing 2 | ( User 3 | , UserDetails 4 | , Username 5 | , decodeDetails 6 | , usernameFromString 7 | , usernameToString 8 | ) 9 | 10 | import Code.Definition.Readme as Readme exposing (Readme) 11 | import Code.Project exposing (ProjectListing) 12 | import Json.Decode as Decode exposing (field, maybe, string) 13 | import Url exposing (Url) 14 | 15 | 16 | type Username 17 | = Username String 18 | 19 | 20 | type alias User u = 21 | { u 22 | | username : Username 23 | , name : Maybe String 24 | , avatarUrl : Maybe Url 25 | } 26 | 27 | 28 | type alias UserDetails = 29 | User { readme : Maybe Readme, projects : List ProjectListing } 30 | 31 | 32 | 33 | -- HELPERS 34 | 35 | 36 | usernameFromString : String -> Maybe Username 37 | usernameFromString raw = 38 | Just (Username raw) 39 | 40 | 41 | usernameToString : Username -> String 42 | usernameToString (Username raw) = 43 | raw 44 | 45 | 46 | 47 | -- DECODE 48 | 49 | 50 | decodeDetails : Decode.Decoder UserDetails 51 | decodeDetails = 52 | let 53 | makeDetails username name avatarUrl readme = 54 | { username = username 55 | , name = name 56 | , avatarUrl = avatarUrl 57 | , readme = readme 58 | , projects = [] 59 | } 60 | in 61 | Decode.map4 makeDetails 62 | (field "fqn" (Decode.map Username string)) 63 | (Decode.succeed Nothing) 64 | (Decode.succeed Nothing) 65 | (maybe (field "readme" Readme.decode)) 66 | -------------------------------------------------------------------------------- /src/assets/FiraCode/FiraCode-VF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/codebase-ui/02cf50b4b8d77325f4b17af8cc12da6b7221e5f4/src/assets/FiraCode/FiraCode-VF.woff2 -------------------------------------------------------------------------------- /src/assets/Inter/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/codebase-ui/02cf50b4b8d77325f4b17af8cc12da6b7221e5f4/src/assets/Inter/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /src/assets/Inter/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/codebase-ui/02cf50b4b8d77325f4b17af8cc12da6b7221e5f4/src/assets/Inter/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /src/assets/Inter/Inter.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/codebase-ui/02cf50b4b8d77325f4b17af8cc12da6b7221e5f4/src/assets/Inter/Inter.var.woff2 -------------------------------------------------------------------------------- /src/assets/Inter/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2020 The Inter Project Authors. 2 | "Inter" is a Reserved Font Name. 3 | https://github.com/rsms/inter 4 | 5 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 6 | This license is copied below, and is also available with a FAQ at: 7 | http://scripts.sil.org/OFL 8 | 9 | ----------------------------------------------------------- 10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 11 | ----------------------------------------------------------- 12 | 13 | PREAMBLE 14 | The goals of the Open Font License (OFL) are to stimulate worldwide 15 | development of collaborative font projects, to support the font creation 16 | efforts of academic and linguistic communities, and to provide a free and 17 | open framework in which fonts may be shared and improved in partnership 18 | with others. 19 | 20 | The OFL allows the licensed fonts to be used, studied, modified and 21 | redistributed freely as long as they are not sold by themselves. The 22 | fonts, including any derivative works, can be bundled, embedded, 23 | redistributed and/or sold with any software provided that any reserved 24 | names are not used by derivative works. The fonts and derivatives, 25 | however, cannot be released under any other type of license. The 26 | requirement for fonts to remain under this license does not apply 27 | to any document created using the fonts or their derivatives. 28 | 29 | DEFINITIONS 30 | "Font Software" refers to the set of files released by the Copyright 31 | Holder(s) under this license and clearly marked as such. This may 32 | include source files, build scripts and documentation. 33 | 34 | "Reserved Font Name" refers to any names specified as such after the 35 | copyright statement(s). 36 | 37 | "Original Version" refers to the collection of Font Software components as 38 | distributed by the Copyright Holder(s). 39 | 40 | "Modified Version" refers to any derivative made by adding to, deleting, 41 | or substituting -- in part or in whole -- any of the components of the 42 | Original Version, by changing formats or by porting the Font Software to a 43 | new environment. 44 | 45 | "Author" refers to any designer, engineer, programmer, technical 46 | writer or other person who contributed to the Font Software. 47 | 48 | PERMISSION AND CONDITIONS 49 | Permission is hereby granted, free of charge, to any person obtaining 50 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 51 | redistribute, and sell modified and unmodified copies of the Font 52 | Software, subject to the following conditions: 53 | 54 | 1) Neither the Font Software nor any of its individual components, 55 | in Original or Modified Versions, may be sold by itself. 56 | 57 | 2) Original or Modified Versions of the Font Software may be bundled, 58 | redistributed and/or sold with any software, provided that each copy 59 | contains the above copyright notice and this license. These can be 60 | included either as stand-alone text files, human-readable headers or 61 | in the appropriate machine-readable metadata fields within text or 62 | binary files as long as those fields can be easily viewed by the user. 63 | 64 | 3) No Modified Version of the Font Software may use the Reserved Font 65 | Name(s) unless explicit written permission is granted by the corresponding 66 | Copyright Holder. This restriction only applies to the primary font name as 67 | presented to the users. 68 | 69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 70 | Software shall not be used to promote, endorse or advertise any 71 | Modified Version, except to acknowledge the contribution(s) of the 72 | Copyright Holder(s) and the Author(s) or with their explicit written 73 | permission. 74 | 75 | 5) The Font Software, modified or unmodified, in part or in whole, 76 | must be distributed entirely under this license, and must not be 77 | distributed under any other license. The requirement for fonts to 78 | remain under this license does not apply to any document created 79 | using the Font Software. 80 | 81 | TERMINATION 82 | This license becomes null and void if any of the above conditions are 83 | not met. 84 | 85 | DISCLAIMER 86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 94 | OTHER DEALINGS IN THE FONT SOFTWARE. 95 | -------------------------------------------------------------------------------- /src/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/unison-share-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/codebase-ui/02cf50b4b8d77325f4b17af8cc12da6b7221e5f4/src/assets/unison-share-social.png -------------------------------------------------------------------------------- /src/css/code.css: -------------------------------------------------------------------------------- 1 | @import "./code/syntax.css"; 2 | @import "./code/finder.css"; 3 | @import "./code/workspace.css"; 4 | @import "./code/definition-doc.css"; 5 | @import "./code/fully-qualified-name.css"; 6 | @import "./code/project-listing.css"; 7 | @import "./code/hashvatar.css"; 8 | -------------------------------------------------------------------------------- /src/css/code/fully-qualified-name.css: -------------------------------------------------------------------------------- 1 | .fully-qualified-name { 2 | display: inline-flex; 3 | align-items: center; 4 | height: 1.5rem; 5 | font-weight: bold; 6 | color: var(--u-color_text); 7 | } 8 | 9 | .fully-qualified-name .fully-qualified-name-separator { 10 | /* em instead of rem to have the margin be relative to the font-size of 11 | * .fully-qualified-name 12 | */ 13 | margin: 0 0.25em; 14 | color: var(--u-color_text_very-subdued); 15 | } 16 | -------------------------------------------------------------------------------- /src/css/code/hashvatar.css: -------------------------------------------------------------------------------- 1 | .hashvatar { 2 | position: relative; 3 | display: inline-flex; 4 | } 5 | -------------------------------------------------------------------------------- /src/css/code/project-listing.css: -------------------------------------------------------------------------------- 1 | .project-listing { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | height: 2rem; 6 | padding: 0 0.25rem; 7 | border-radius: var(--border-radius-base); 8 | } 9 | 10 | .project-listing:hover { 11 | text-decoration: none; 12 | background: var(--u-color_element_hovered); 13 | } 14 | 15 | .project-listing .hashvatar { 16 | margin-right: 0.5rem; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/css/code/workspace.css: -------------------------------------------------------------------------------- 1 | #workspace { 2 | --color-workspace-gutter: var(--u-color_container_subdued); 3 | --workspace-content-width: var(--main-content-width); 4 | 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | #workspace-content { 10 | overflow: auto; 11 | height: calc(calc(100vh - var(--toolbar-height)) - var(--app-header-height)); 12 | padding-top: 2rem; 13 | scroll-behavior: smooth; 14 | scrollbar-width: auto; 15 | scrollbar-color: var(--u-color_scrollbar) var(--u-color_scrollbar-track); 16 | /* gutter */ 17 | box-shadow: inset 2rem 0 0 var(--color-workspace-gutter); 18 | } 19 | 20 | #workspace-content::-webkit-scrollbar { 21 | width: 0.5rem; 22 | height: 0.5rem; 23 | } 24 | 25 | #workspace-content::-webkit-scrollbar-track { 26 | background: var(--u-color_scrollbar-track); 27 | } 28 | 29 | #workspace-content::-webkit-scrollbar-thumb { 30 | background-color: var(--u-color_scrollbar); 31 | border-radius: var(--border-radius-base); 32 | } 33 | 34 | @media only screen and (max-width: 1024px) { 35 | #workspace-content { 36 | box-shadow: none; 37 | height: auto; 38 | width: auto; 39 | } 40 | } 41 | 42 | @import "./workspace-item.css"; 43 | -------------------------------------------------------------------------------- /src/css/ui.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --size-base: 16px; 3 | --app-header-height: 3.5rem; 4 | --page-hero-height: 16rem; 5 | --main-sidebar-width: 17rem; 6 | --main-sidebar-collapsed-width: 4.5rem; 7 | --main-content-width: 45.5rem; 8 | --border-radius-base: 0.25rem; 9 | --toolbar-height: 3.5rem; 10 | 11 | /* -- Layers ------------------------------------------------------------- */ 12 | --layer-beneath: 0; 13 | --layer-base: 1; 14 | --layer-popover: 50; 15 | --layer-tooltip: 75; 16 | --layer-modal-overlay: 99; 17 | --layer-modal: 100; 18 | --layer-modal-above: 110; 19 | 20 | /* -- Font --------------------------------------------------------------- */ 21 | --font-sans-serif: "Inter var", sans-serif; 22 | --font-monospace: "Fira Code var", monospace; 23 | 24 | --font-size-base: 1rem; 25 | --font-size-large: 1.5rem; 26 | --font-size-medium: 0.875rem; 27 | --font-size-small: 0.75rem; 28 | } 29 | 30 | @import "./ui/colors.css"; 31 | @import "./ui/fonts.css"; 32 | @import "./ui/base.css"; 33 | @import "./ui/animations.css"; 34 | @import "./ui/components.css"; 35 | @import "./ui/page-layout.css"; 36 | -------------------------------------------------------------------------------- /src/css/ui/animations.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --anim-elastic: cubic-bezier(0.68, -0.4, 0.32, 1.4); 3 | } 4 | 5 | @keyframes fade-in { 6 | 0% { 7 | opacity: 0; 8 | } 9 | 100% { 10 | opacity: 1; 11 | } 12 | } 13 | 14 | @keyframes fade-in-out { 15 | 0% { 16 | opacity: 0; 17 | } 18 | 50% { 19 | opacity: 1; 20 | } 21 | 100% { 22 | opacity: 0; 23 | } 24 | } 25 | 26 | @keyframes slide-up { 27 | 0% { 28 | opacity: 0; 29 | transform: translateY(1rem); 30 | } 31 | 100% { 32 | opacity: 1; 33 | transform: translateY(0); 34 | } 35 | } 36 | 37 | @keyframes slide-down { 38 | 0% { 39 | opacity: 0; 40 | transform: translateY(-1rem); 41 | } 42 | 100% { 43 | opacity: 1; 44 | transform: translateY(0); 45 | } 46 | } 47 | 48 | @keyframes spin { 49 | 100% { 50 | transform: rotate(360deg); 51 | } 52 | } 53 | 54 | /* scale3D instead of scale forces the animation onto the GPU, avoiding jank */ 55 | @keyframes pulse-grow { 56 | 0% { 57 | transform: scale3D(1, 1, 1); 58 | } 59 | 50% { 60 | transform: scale3D(1.25, 1.25, 1.25); 61 | } 62 | 100% { 63 | transform: scale3D(1, 1, 1); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/css/ui/base.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body, 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | ul, 12 | p, 13 | ol, 14 | table, 15 | tr, 16 | td, 17 | li { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | table { 23 | border-collapse: collapse; 24 | border-spacing: 0; 25 | } 26 | 27 | html { 28 | font-size: var(--size-base); 29 | font-family: var(--font-sans-serif); 30 | /* TODO: Make configurable by users */ 31 | /* https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant-ligatures */ 32 | font-variant-ligatures: none; 33 | /* normal */ 34 | -moz-text-size-adjust: none; 35 | -webkit-text-size-adjust: none; 36 | text-size-adjust: none; 37 | } 38 | 39 | body { 40 | font-size: 1rem; 41 | line-height: 1.4; 42 | background: var(--u-color_background); 43 | color: var(--u-color_text); 44 | } 45 | 46 | input { 47 | border: 0; 48 | border-radius: 0; 49 | font-family: var(--font-sans-serif); 50 | caret-color: var(--u-color_caret); 51 | } 52 | 53 | ::placeholder { 54 | color: var(--u-color_text_subdued); 55 | } 56 | 57 | li::marker { 58 | color: var(--u-color_icon); 59 | } 60 | 61 | mark { 62 | color: var(--u-color_search-highlight_text); 63 | background: var(--u-color_search-highlight_background); 64 | } 65 | -------------------------------------------------------------------------------- /src/css/ui/colors.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* `transparent` sometimes renders as a dark shade when used 3 | * in gradients in some browsers. Having a variable based 4 | * on rgba fixes this and makes it convienient to use 5 | * everywhere. 6 | * https://stackoverflow.com/a/56548711 7 | * 8 | * TODO: Just use RGB instead of HEX 9 | * https://stackoverflow.com/a/41265350 10 | */ 11 | --color-transparent: rgba(255, 255, 255, 0); 12 | --color-gray-darken-30-20pct: rgba(24, 24, 28, 0.2); 13 | --color-gray-darken-30-50pct: rgba(24, 24, 28, 0.5); 14 | --color-gray-darken-20-transparent: rgba(45, 46, 53, 0); 15 | --color-gray-darken-10-transparent: rgba(65, 66, 75, 0); 16 | --color-gray-lighten-100-50pct: rgba(255, 255, 255, 0.5); 17 | --color-gray-lighten-60-50pct: rgba(250, 250, 251, 0.5); 18 | 19 | /* Grays */ 20 | --color-gray-darken-30: #18181c; 21 | --color-gray-darken-25: #22232a; 22 | --color-gray-darken-20: #2d2e35; 23 | --color-gray-darken-10: #41424b; 24 | 25 | --color-gray-base: #515258; 26 | 27 | --color-gray-lighten-20: #818286; 28 | --color-gray-lighten-30: #bdbfc6; 29 | --color-gray-lighten-40: #d1d5dc; 30 | --color-gray-lighten-45: #d9e0e7; 31 | --color-gray-lighten-50: #e4eaf3; 32 | --color-gray-lighten-55: #f1f3f5; 33 | --color-gray-lighten-60: #fafafb; 34 | --color-gray-lighten-100: #ffffff; 35 | 36 | /* Pinks */ 37 | --color-pink-1: #ff4756; 38 | --color-pink-2: #ff6c78; 39 | --color-pink-3: #ff9ba3; 40 | 41 | /* Greens */ 42 | --color-green-1: #27ae60; 43 | --color-green-2: #52d188; 44 | --color-green-3: #88f3b5; 45 | --color-green-4: #c6ffde; 46 | 47 | /* Blues */ 48 | --color-blue-1: #225ebe; 49 | --color-blue-2: #5695f4; 50 | --color-blue-3: #9ec5ff; 51 | --color-blue-4: #cbe0ff; 52 | --color-blue-5: #ecf2fa; 53 | --color-blue-2-25pct: rgba(85, 149, 255, 0.25); 54 | 55 | /* Oranges */ 56 | --color-orange-1: #ff8800; 57 | --color-orange-2: #ffc41f; 58 | --color-orange-3: #ffe08b; 59 | --color-orange-4: #ffeebe; 60 | --color-orange-5: #fff7df; 61 | 62 | /* Purples */ 63 | --color-purple-1: #55377b; 64 | --color-purple-2: #734da3; 65 | --color-purple-3: #9a76c8; 66 | --color-purple-4: #c6a8ec; 67 | } 68 | -------------------------------------------------------------------------------- /src/css/ui/components.css: -------------------------------------------------------------------------------- 1 | @import "./components/icon.css"; 2 | @import "./components/text.css"; 3 | @import "./components/button.css"; 4 | @import "./components/tooltip.css"; 5 | @import "./components/fold-toggle.css"; 6 | @import "./components/card.css"; 7 | @import "./components/toolbar.css"; 8 | @import "./components/app-header.css"; 9 | @import "./components/modal.css"; 10 | @import "./components/codebase-tree.css"; 11 | @import "./components/copy-field.css"; 12 | @import "./components/keyboard-shortcuts.css"; 13 | @import "./components/badges.css"; 14 | @import "./components/divider.css"; 15 | @import "./components/loading-placeholder.css"; 16 | -------------------------------------------------------------------------------- /src/css/ui/components/app-header.css: -------------------------------------------------------------------------------- 1 | /* -- App Header ---------------------------------------------------------- */ 2 | 3 | #app-header { 4 | /* @color-todo @inverse */ 5 | --color-app-header-fg: var(--color-gray-lighten-100); 6 | --color-app-header-bg: var(--color-gray-darken-10); 7 | --color-app-header-subtle-fg: var(--color-gray-lighten-20); 8 | --color-app-header-subtle-fg-em: var(--color-gray-lighten-50); 9 | --color-app-header-context-unison-share-fg: var(--color-purple-4); 10 | --color-app-header-context-unison-local-fg: var(--color-pink-3); 11 | --color-app-header-border: transparent; 12 | grid-area: app-header; 13 | padding: 0 1rem 0 1.5rem; 14 | background: var(--color-app-header-bg); 15 | color: var(--color-app-header-fg); 16 | border-bottom: 1px solid var(--color-app-header-border); 17 | display: flex; 18 | align-items: center; 19 | font-size: 1rem; 20 | height: var(--app-header-height); 21 | } 22 | 23 | #app-header .menu-toggle { 24 | display: none; /* enabled on mobile */ 25 | line-height: 1.5rem; 26 | margin-right: 0.5rem; 27 | } 28 | 29 | #app-header .menu-toggle .icon { 30 | color: var(--color-app-header-subtle-fg); 31 | font-size: 1.5rem; 32 | } 33 | #app-header .menu-toggle:hover .icon { 34 | color: var(--color-app-header-subtle-fg-em); 35 | } 36 | 37 | #app-header .app-title { 38 | font-size: var(--font-size-base); 39 | display: inline-flex; 40 | flex-direction: row; 41 | align-items: center; 42 | color: var(--color-app-header-fg); 43 | } 44 | 45 | #app-header .app-title h1 { 46 | font-size: var(--font-size-base); 47 | } 48 | 49 | #app-header .app-title:hover { 50 | text-decoration: none; 51 | transform: translate(0, 0.1rem); 52 | } 53 | 54 | #app-header .app-title .context { 55 | margin-left: 0.375rem; 56 | } 57 | 58 | #app-header .app-title .unison-share { 59 | color: var(--color-app-header-context-unison-share-fg); 60 | } 61 | 62 | #app-header .app-title .unison-local { 63 | color: var(--color-app-header-context-unison-local-fg); 64 | } 65 | 66 | #app-header .right { 67 | margin-left: auto; 68 | justify-self: flex-end; 69 | align-items: center; 70 | } 71 | 72 | #app-header .right .banner { 73 | margin-right: 0.75rem; 74 | } 75 | 76 | /* -- Responsive ----------------------------------------------------------- */ 77 | 78 | @media only screen and (max-width: 1024px) { 79 | #app-header { 80 | padding: 0 1rem; 81 | } 82 | 83 | #app-header .menu-toggle { 84 | display: flex; 85 | } 86 | 87 | #app-header .banner { 88 | display: none; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/css/ui/components/badges.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | --color-badge-text: var(--u-color_text-on-element-emphasized); 3 | --color-badge-bg: var(--u-color_element_emphasized); 4 | --color-badge-border: var(--u-color_border); 5 | 6 | color: var(--color-badge-text); 7 | background: var(--color-badge-bg); 8 | border: 1px solid var(--color-badge-border); 9 | font-size: var(--font-size-small); 10 | height: 1.5rem; 11 | display: flex; 12 | align-items: center; 13 | padding: 0 0.5rem; 14 | border-radius: var(--border-radius-base); 15 | } 16 | 17 | .option-badge { 18 | /* @color-todo @inverse */ 19 | --color-option-badge-text: var(--color-gray-lighten-40); 20 | --color-option-badge-subtle-text: var(--color-gray-lighten-20); 21 | --color-option-badge-icon: var(--color-gray-lighten-30); 22 | --color-option-badge-hover-icon: var(--color-gray-100); 23 | --color-option-badge-bg: var(--color-gray-darken-30); 24 | --color-option-badge-border: var(--color-transparent); 25 | 26 | padding: 0 0.75rem; 27 | align-items: center; 28 | display: inline-flex; 29 | flex-direction: row; 30 | height: 1.5rem; 31 | border-radius: calc(1.5rem / 2); 32 | border: 1px solid var(--color-option-badge-border); 33 | background: var(--color-option-badge-bg); 34 | color: var(--color-option-badge-text); 35 | font-size: var(--font-size-small); 36 | cursor: pointer; 37 | } 38 | 39 | .option-badge .icon { 40 | font-size: 0.625rem; 41 | color: var(--color-option-badge-icon); 42 | margin-right: 0.375rem; 43 | line-height: 1; 44 | } 45 | 46 | .option-badge:hover .icon { 47 | color: var(--color-option-badge-hover-icon); 48 | } 49 | 50 | .option-badge .subtle { 51 | color: var(--color-option-badge-subtle-text); 52 | } 53 | -------------------------------------------------------------------------------- /src/css/ui/components/card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | --color-card-bg: var(--u-color_container); 3 | --color-card-text: var(--u-color_text); 4 | --color-card-title: var(--u-color_text_very-subdued); 5 | --color-card-border: var(--u-color_border_subdued); 6 | 7 | padding: 1rem 1.5rem; 8 | display: flex; 9 | flex-direction: column; 10 | gap: 1rem; 11 | color: var(--color-card-text); 12 | border-radius: var(--border-radius-base); 13 | background: var(--color-card-bg); 14 | } 15 | 16 | .card.contained { 17 | border: 1px solid var(--color-card-border); 18 | } 19 | 20 | .card .card-title { 21 | color: var(--color-card-title); 22 | text-transform: uppercase; 23 | font-size: var(--font-size-small); 24 | font-weight: normal; 25 | } 26 | -------------------------------------------------------------------------------- /src/css/ui/components/codebase-tree.css: -------------------------------------------------------------------------------- 1 | /* -- Codebase Tree -------------------------------------------------------- */ 2 | 3 | .codebase-tree .namespace-tree { 4 | margin-left: -0.5rem; 5 | } 6 | 7 | .codebase-tree .namespace-tree .node { 8 | display: flex; 9 | user-select: none; 10 | align-items: center; 11 | border-radius: var(--border-radius-base); 12 | padding-left: 0.5rem; 13 | margin-bottom: 0.125rem; 14 | padding-right: 0.1875rem; 15 | height: 1.875rem; 16 | } 17 | 18 | .codebase-tree .error { 19 | padding-left: 0.5rem; 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | margin: 0.5rem 0; 24 | color: var(--u-color_critical_text); 25 | } 26 | 27 | .codebase-tree .error .icon { 28 | font-size: 1rem; 29 | margin-right: 0.25rem; 30 | color: var(--u-color_critical_icon); 31 | } 32 | 33 | .codebase-tree .loading { 34 | padding-left: 0.5rem; 35 | } 36 | 37 | .codebase-tree .loading .loading-placeholder { 38 | width: 8rem; 39 | } 40 | 41 | .codebase-tree .namespace-content .loading { 42 | padding-left: 0.875rem; 43 | } 44 | 45 | .codebase-tree .namespace-content .loading .loading-placeholder { 46 | width: 6rem; 47 | } 48 | 49 | .codebase-tree .namespace-tree .node:hover { 50 | background: var(--color-sidebar-focus-bg); 51 | text-decoration: none; 52 | } 53 | 54 | .codebase-tree .namespace-tree .node > .icon { 55 | font-size: 0.875rem; 56 | text-align: center; 57 | margin-right: 0.5rem; 58 | transition: transform 0.1s ease-out; 59 | flex-shrink: 0; 60 | } 61 | 62 | .codebase-tree .namespace-tree .node > .icon.expanded { 63 | transform: rotate(90deg); 64 | } 65 | 66 | .codebase-tree .namespace-tree .node > .icon.caret-right { 67 | color: var(--color-sidebar-subtle-fg); 68 | } 69 | 70 | .codebase-tree .namespace-tree .node:hover > .icon.caret-right { 71 | color: var(--color-sidebar-focus-fg); 72 | } 73 | 74 | .codebase-tree .namespace-tree .node > label { 75 | color: var(--color-sidebar-fg); 76 | transition: all 0.2s; 77 | cursor: pointer; 78 | overflow: hidden; 79 | text-overflow: ellipsis; 80 | line-height: 1.875; 81 | } 82 | 83 | .codebase-tree .namespace-tree .node > .tooltip-trigger { 84 | margin-left: auto; 85 | opacity: 0; 86 | } 87 | 88 | .codebase-tree .namespace-tree .node:hover > .tooltip-trigger { 89 | opacity: 1; 90 | } 91 | 92 | .codebase-tree .namespace-tree .node:hover .tooltip { 93 | right: -0.3rem; 94 | min-width: calc(var(--main-sidebar-width) - 1.5rem); 95 | } 96 | 97 | .codebase-tree .namespace-tree .node .definition-category { 98 | font-family: var(--font-monospace); 99 | color: var(--color-sidebar-subtle-fg); 100 | margin-left: 0.375rem; 101 | font-size: 0.75rem; 102 | margin-top: 0.25rem; 103 | } 104 | 105 | .codebase-tree .namespace-tree .node:hover label { 106 | color: var(--color-sidebar-focus-fg); 107 | } 108 | 109 | .codebase-tree .namespace-tree .node.open label { 110 | font-weight: bold; 111 | } 112 | 113 | .codebase-tree .namespace-tree .namespace-content { 114 | margin-left: 1rem; 115 | } 116 | -------------------------------------------------------------------------------- /src/css/ui/components/copy-field.css: -------------------------------------------------------------------------------- 1 | .copy-field { 2 | --color-copy-field-text: var(--u-color_text); 3 | --color-copy-field-bg: var(--u-color_element_disabled); 4 | --color-copy-field-border: var(--u-color_border); 5 | --color-copy-field-focus-border: var(--u-color_focus-border); 6 | --color-copy-field-focus-outline: var(--u-color_focus-outline); 7 | --color-copy-field-prefix: var(--u-color_text_subdued); 8 | --color-copy-field-button-border: var(--u-color_border); 9 | --color-copy-field-button-hover-border: var(--color-gray-lighten-30); 10 | 11 | position: relative; 12 | display: flex; 13 | flex-direction: row; 14 | height: 2.25rem; 15 | font-family: var(--font-monospace); 16 | 17 | --height-without-border: calc(2.25rem - 2px); 18 | } 19 | 20 | .copy-field .copy-field-field { 21 | position: relative; 22 | display: flex; 23 | flex-direction: row; 24 | background: var(--color-copy-field-bg); 25 | border: 1px solid var(--color-copy-field-border); 26 | border-radius: var(--border-radius-base) 0 0 var(--border-radius-base); 27 | flex-grow: 1; 28 | } 29 | 30 | .copy-field .copy-field-field:focus-within { 31 | box-shadow: 0 0 0 2px var(--color-copy-field-focus-outline); 32 | border-color: var(--color-copy-field-focus-border); 33 | border-right: 1px solid var(--color-copy-field-focus-border); 34 | /* z-index is to help cover the left border of the button when focused */ 35 | z-index: 2; 36 | } 37 | 38 | .copy-field .copy-field-prefix { 39 | height: var(--height-without-border); 40 | padding: 0 0.5ch 0 0.5rem; 41 | font-size: var(--font-size-medium); 42 | align-items: center; 43 | display: flex; 44 | color: var(--color-copy-field-prefix); 45 | } 46 | 47 | .copy-field .copy-field-input { 48 | flex-grow: 1; 49 | } 50 | 51 | .copy-field input { 52 | width: 100%; 53 | font-family: var(--font-monospace); 54 | height: var(--height-without-border); 55 | font-size: var(--font-size-medium); 56 | font-weight: 600; 57 | background: transparent; 58 | color: var(--color-copy-field-text); 59 | } 60 | 61 | .copy-field input:focus { 62 | outline: none; 63 | } 64 | 65 | /* TODO: Should this button be more aligned with other buttons in the app? */ 66 | .copy-field button { 67 | width: 2.25rem; 68 | height: 2.25rem; 69 | border: 1px solid var(--color-copy-field-button-border); 70 | border-radius: 0 var(--border-radius-base) var(--border-radius-base) 0; 71 | /* move 1 px left such that borders of field and button overlap 72 | * (visible when clicking the button) */ 73 | margin-left: -1px; 74 | position: relative; 75 | } 76 | 77 | .copy-field button:hover { 78 | border-color: var(--color-copy-field-button-hover-border); 79 | /* z-index is to show the buttons left border on hover */ 80 | z-index: 1; 81 | } 82 | 83 | .copy-field button .icon { 84 | font-size: 2.25rem; 85 | } 86 | -------------------------------------------------------------------------------- /src/css/ui/components/divider.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | background: var(--u-color_divider); 3 | border: 0; 4 | margin: 1.5rem 0; 5 | height: 2px; 6 | border-radius: 1px; 7 | } 8 | -------------------------------------------------------------------------------- /src/css/ui/components/fold-toggle.css: -------------------------------------------------------------------------------- 1 | .fold-toggle { 2 | --color-fold-toggle-icon: var(--u-color_icon-on-action-subdued); 3 | --color-fold-toggle-bg: var(--u-color_action_subdued); 4 | --color-fold-toggle-hover-icon: var(--u-color_icon-on-action-subdued-hovered); 5 | --color-fold-toggle-hover-bg: var(--u-color_action_subdued_hovered); 6 | 7 | display: inline-flex; 8 | align-items: center; 9 | justify-content: center; 10 | flex-direction: row; 11 | width: 1.25rem; 12 | height: 1.25rem; 13 | margin-right: 0.25rem; 14 | line-height: 1; 15 | border-radius: var(--border-radius-base); 16 | transition: all 0.2s; 17 | cursor: pointer; 18 | background: var(--color-fold-toggle-bg); 19 | } 20 | 21 | .fold-toggle.disabled { 22 | cursor: default; 23 | opacity: 0.5; 24 | } 25 | 26 | /* ▶ */ 27 | .fold-toggle .icon { 28 | color: var(--color-fold-toggle-icon); 29 | transition: color 0.2s, transform 0.1s ease-out; 30 | } 31 | 32 | /* rotate ▶ to ▼ */ 33 | .fold-toggle.folded-open { 34 | transform: rotate(90deg); 35 | } 36 | 37 | .fold-toggle:not(.disabled):hover { 38 | background: var(--color-fold-toggle-hover-bg); 39 | } 40 | 41 | .fold-toggle:not(.disabled):hover .icon { 42 | color: var(--color-fold-toggle-hover-icon); 43 | } 44 | -------------------------------------------------------------------------------- /src/css/ui/components/icon.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: inline-block; 3 | font-size: 0.875rem; 4 | width: 1em; 5 | height: 1em; 6 | color: var(--u-color_icon); 7 | transition: all 0.2s; 8 | } 9 | 10 | .icon.type, 11 | .icon.data-constructor { 12 | color: var(--color-icon-type); 13 | } 14 | 15 | .icon.test { 16 | color: var(--color-icon-test); 17 | } 18 | 19 | .icon.term { 20 | color: var(--color-icon-term); 21 | } 22 | 23 | .icon.ability, 24 | .icon.ability-constructor { 25 | color: var(--color-icon-ability); 26 | } 27 | 28 | .icon.doc { 29 | color: var(--color-icon-doc); 30 | } 31 | 32 | .icon.patch { 33 | color: var(--color-icon-patch); 34 | } 35 | 36 | .icon.animate.search .search-shine, 37 | .icon.animate.search .search-shine-trail-1, 38 | .icon.animate.search .search-shine-trail-2, 39 | .icon.animate.search .search-shine-trail-3 { 40 | animation: spin 2s var(--anim-elastic) infinite; 41 | transform-origin: 6px 6px; 42 | will-change: transform; 43 | } 44 | 45 | .icon.animate.search .search-shine { 46 | fill: var(--color-icon-1, currentColor); 47 | animation-delay: var(--icon-animation-delay, 0s); 48 | } 49 | 50 | .icon.animate.search .search-shine-trail-1 { 51 | fill: var(--color-icon-2, var(--color-transparent, transparent)); 52 | animation-delay: calc(0.1s + var(--icon-animation-delay, 0s)); 53 | transition-delay: var(--icon-transition-delay, 0s); 54 | } 55 | 56 | .icon.animate.search .search-shine-trail-2 { 57 | fill: var(--color-icon-3, var(--color-transparent, transparent)); 58 | animation-delay: calc(0.2s + var(--icon-animation-delay, 0s)); 59 | transition-delay: var(--icon-transition-delay, 0s); 60 | } 61 | 62 | .icon.animate.search .search-shine-trail-3 { 63 | fill: var(--color-icon-4, var(--color-transparent, transparent)); 64 | animation-delay: calc(0.3s + var(--icon-animation-delay, 0s)); 65 | transition-delay: var(--icon-transition-delay, 0s); 66 | } 67 | -------------------------------------------------------------------------------- /src/css/ui/components/keyboard-shortcuts.css: -------------------------------------------------------------------------------- 1 | .keyboard-shortcuts { 2 | --color-keyboard-shortcut-key-text: var(--u-color_text-on-element-emphasized); 3 | --color-keyboard-shortcut-key-bg: var(--u-color_element_emphasized); 4 | --color-keyboard-shortcut-then: var(--u-color_text_subdued); 5 | --color-keyboard-shortcut-separator: var(--u-color_text_subdued); 6 | 7 | display: flex; 8 | flex-direction: row; 9 | justify-self: flex-end; 10 | margin-left: auto; 11 | } 12 | 13 | .keyboard-shortcuts .separator { 14 | display: inline-flex; 15 | height: 1.5rem; 16 | font-size: 0.875rem; 17 | color: var(--color-keyboard-shortcut-separator); 18 | align-items: center; 19 | margin: 0 0.4rem; 20 | line-height: 1; 21 | } 22 | 23 | .keyboard-shortcut { 24 | display: flex; 25 | flex-direction: row; 26 | /* gap: 0.25rem; */ 27 | } 28 | 29 | .keyboard-shortcut .key { 30 | margin-right: 0.25rem; 31 | } 32 | 33 | .keyboard-shortcut:last-child .key { 34 | margin-right: 0; 35 | } 36 | 37 | .keyboard-shortcut .key:last-child { 38 | margin-right: 0; 39 | } 40 | 41 | .keyboard-shortcut .key { 42 | height: 1.5rem; 43 | min-width: 1.5rem; 44 | font-size: 0.75rem; 45 | padding: 0.375rem; 46 | border-radius: var(--border-radius-base); 47 | text-align: center; 48 | display: inline-flex; 49 | justify-content: center; 50 | align-content: center; 51 | align-items: center; 52 | color: var(--color-keyboard-shortcut-key-text); 53 | background: var(--color-keyboard-shortcut-key-bg); 54 | transition: all 0.2s; 55 | } 56 | 57 | .keyboard-shortcut .then { 58 | display: inline-flex; 59 | height: 1.5rem; 60 | font-size: 0.625rem; 61 | line-height: 1; 62 | margin: 0 0.35rem; 63 | color: var(--color-keyboard-shortcut-then); 64 | align-items: center; 65 | } 66 | -------------------------------------------------------------------------------- /src/css/ui/components/loading-placeholder.css: -------------------------------------------------------------------------------- 1 | .loading-placeholder { 2 | --color-placeholder: var(--u-color_text_very-subdued); 3 | 4 | display: inline-block; 5 | height: calc(var(--font-size-base) * 0.65); 6 | border-radius: calc(var(--font-size-base) / 2); 7 | background: var(--color-placeholder); 8 | min-width: calc(var(--font-size-base) * 3); 9 | } 10 | 11 | .loading-placeholder-row { 12 | margin-bottom: 0.5rem; 13 | } 14 | -------------------------------------------------------------------------------- /src/css/ui/components/modal.css: -------------------------------------------------------------------------------- 1 | #modal-overlay { 2 | /* @color-todo */ 3 | --color-modal-fg: var(--u-color_text); 4 | --color-modal-mg: var(--u-color_element_subdued); 5 | --color-modal-bg: var(--u-color_container); 6 | --color-modal-inner-border: var(--color-gray-lighten-50); 7 | --color-modal-separator: var(--u-color_divider); 8 | --color-modal-shadow: var(--u-shadow); 9 | --color-modal-overlay: var(--u-color_backdrop); 10 | --color-modal-border: var(--color-transparent); 11 | --color-modal-subtle-fg: var(--u-color_text_very-subdued); 12 | --color-modal-subtle-fg-em: var(--u-color-text-subdued); 13 | --color-modal-subtle-mg: var(--color-gray-lighten-55); 14 | --color-modal-subtle-bg: var(--u-color_container_subdued); 15 | --color-modal-focus-fg: var(--u-color_text); 16 | --color-modal-focus-bg: var(--color-gray-lighten-55); 17 | --color-modal-focus-subtle-fg: var(--color-gray-base); 18 | --color-modal-focus-subtle-bg: var(--color-gray-lighten-50); 19 | --color-modal-title-fg: var(--color-gray-lighten-20); 20 | --color-modal-title-bg: var(--color-transparent); 21 | --color-modal-error-fg: var(--color-pink-1); 22 | 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | background: var(--color-modal-overlay); 29 | display: flex; 30 | flex: 1; 31 | flex-shrink: 3; 32 | justify-content: center; 33 | animation: fade-in 0.2s ease-out; 34 | z-index: var(--layer-modal-overlay); 35 | } 36 | 37 | .modal { 38 | position: relative; 39 | background: var(--color-modal-bg); 40 | border-radius: var(--border-radius-base); 41 | width: auto; 42 | margin-top: 4rem; 43 | height: -moz-fit-content; 44 | height: fit-content; 45 | animation: slide-up 0.2s var(--anim-elastic); 46 | box-shadow: 0 0.375rem 1rem var(--color-modal-shadow); 47 | z-index: var(--layer-modal); 48 | font-size: var(--font-size-medium); 49 | } 50 | 51 | .modal-header { 52 | padding: 1.5rem; 53 | display: flex; 54 | flex-direction: row; 55 | } 56 | 57 | .modal-header + .modal-content { 58 | padding-top: 0.5rem; 59 | } 60 | 61 | .modal-header h2 { 62 | font-size: var(--font-size-base); 63 | height: 1.5rem; 64 | color: var(--color-modal-title-fg); 65 | } 66 | 67 | .modal-header .close-modal { 68 | width: 1.5rem; 69 | height: 1.5rem; 70 | font-size: var(--font-size-medium); 71 | justify-self: right; 72 | margin-top: -0.5rem; 73 | margin-right: -1rem; 74 | margin-left: auto; 75 | } 76 | 77 | .modal-header .close-modal .icon { 78 | color: var(--color-modal-subtle-fg); 79 | } 80 | 81 | .modal-header .close-modal:hover .icon { 82 | color: var(--color-modal-fg); 83 | } 84 | 85 | .modal-content { 86 | padding: 1.5rem; 87 | } 88 | 89 | .modal:focus { 90 | outline: none; 91 | } 92 | 93 | .hint { 94 | font-size: var(--font-size-small); 95 | color: var(--color-modal-subtle-fg-em); 96 | } 97 | -------------------------------------------------------------------------------- /src/css/ui/components/text.css: -------------------------------------------------------------------------------- 1 | a, 2 | a:visited { 3 | text-decoration: none; 4 | cursor: pointer; 5 | transition: all 0.2s; 6 | color: var(--u-color_interactive); 7 | } 8 | 9 | a:hover { 10 | color: var(--u-color_interactive_hovered); 11 | } 12 | 13 | a:active { 14 | color: var(--u-color_interactive_pressed); 15 | } 16 | 17 | h1 { 18 | font-size: var(--font-size-large); 19 | } 20 | 21 | h2 { 22 | font-size: var(--font-size-base); 23 | } 24 | 25 | h3 { 26 | font-size: var(--font-size-medium); 27 | } 28 | 29 | p { 30 | margin-bottom: 1em; 31 | } 32 | 33 | .error-message { 34 | color: var(--u-color_critical_text); 35 | } 36 | 37 | .subtle { 38 | color: var(--u-color_text_subdued); 39 | } 40 | -------------------------------------------------------------------------------- /src/css/ui/components/toolbar.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | --color-toolbar: var(--u-color_container); 3 | --color-toolbar-border: var(--u-color_border_subdued); 4 | 5 | height: var(--toolbar-height); 6 | padding-left: 2.625rem; 7 | padding-right: 1rem; 8 | font-size: var(--font-size-medium); 9 | background: var(--color-toolbar); 10 | border-bottom: 1px solid var(--color-toolbar-border); 11 | display: flex; 12 | flex-direction: row; 13 | align-items: center; 14 | } 15 | 16 | .toolbar .right { 17 | margin-left: auto; 18 | } 19 | -------------------------------------------------------------------------------- /src/css/ui/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Inter var"; 3 | font-weight: 100 900; 4 | font-display: block; 5 | font-style: normal; 6 | font-named-instance: "Regular"; 7 | src: url("assets/Inter/Inter-roman.var.woff2") format("woff2"); 8 | } 9 | @font-face { 10 | font-family: "Inter var"; 11 | font-weight: 100 900; 12 | font-display: block; 13 | font-style: italic; 14 | font-named-instance: "Italic"; 15 | src: url("assets/Inter/Inter-italic.var.woff2") format("woff2"); 16 | } 17 | 18 | @font-face { 19 | font-family: "Fira Code var"; 20 | font-weight: 100 900; 21 | font-display: block; 22 | font-style: normal; 23 | font-named-instance: "Regular"; 24 | src: url("assets/FiraCode/FiraCode-VF.woff2") format("woff2"); 25 | } 26 | -------------------------------------------------------------------------------- /src/css/unison-local.css: -------------------------------------------------------------------------------- 1 | #app { 2 | display: grid; 3 | grid-template-rows: var(--app-header-height) auto; 4 | grid-template-columns: auto; 5 | grid-template-areas: 6 | "app-header" 7 | "page-layout"; 8 | } 9 | 10 | /* -- App Error ---------------------------------------------------------- */ 11 | 12 | .app-error { 13 | display: flex; 14 | flex: 1; 15 | flex-direction: column; 16 | align-items: center; 17 | padding-top: 8rem; 18 | font-weight: 600; 19 | color: var(--u-color_text); 20 | } 21 | 22 | .app-error .icon { 23 | font-size: 4rem; 24 | color: var(--u-color_critical_icon); 25 | } 26 | 27 | @import "./unison-local/help-modal.css"; 28 | @import "./unison-local/publish-modal.css"; 29 | @import "./unison-local/report-bug-modal.css"; 30 | @import "./unison-local/perspective-landing.css"; 31 | -------------------------------------------------------------------------------- /src/css/unison-local/help-modal.css: -------------------------------------------------------------------------------- 1 | #help-modal .shortcuts { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 2.625rem; 5 | } 6 | 7 | #help-modal .shortcuts .shortcut-group { 8 | width: 20rem; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 0.75rem; 12 | } 13 | 14 | #help-modal .shortcuts h3 { 15 | height: 1.5rem; 16 | margin-bottom: 1rem; 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | #help-modal .shortcuts .row { 22 | display: flex; 23 | height: 1.5rem; 24 | align-items: center; 25 | } 26 | 27 | #help-modal .shortcuts .instructions { 28 | display: flex; 29 | flex-direction: row; 30 | justify-self: flex-end; 31 | margin-left: auto; 32 | } 33 | 34 | #help-modal .subtle { 35 | color: var(--color-modal-subtle-fg); 36 | } 37 | -------------------------------------------------------------------------------- /src/css/unison-local/publish-modal.css: -------------------------------------------------------------------------------- 1 | #publish-modal { 2 | width: 32.5rem; 3 | } 4 | 5 | #publish-modal .main { 6 | font-size: var(--font-size-medium); 7 | } 8 | 9 | #publish-modal .help { 10 | display: inline-block; 11 | margin-top: 1rem; 12 | font-size: var(--font-size-small); 13 | color: var(--color-modal-subtle-fg-em); 14 | text-decoration: underline; 15 | } 16 | 17 | #publish-modal .help:hover { 18 | color: var(--color-modal-fg); 19 | } 20 | -------------------------------------------------------------------------------- /src/css/unison-local/report-bug-modal.css: -------------------------------------------------------------------------------- 1 | #report-bug-modal { 2 | width: 32.5rem; 3 | } 4 | 5 | #report-bug-modal .actions .button { 6 | margin-right: 0.5rem; 7 | } 8 | 9 | #report-bug-modal .actions .action { 10 | display: flex; 11 | align-items: center; 12 | margin-bottom: 1rem; 13 | } 14 | 15 | #report-bug-modal .actions .action strong { 16 | margin: 0 0.25rem; 17 | } 18 | 19 | #report-bug-modal .actions .action:last-child { 20 | margin-bottom: 0; 21 | } 22 | -------------------------------------------------------------------------------- /src/css/unison-share.css: -------------------------------------------------------------------------------- 1 | #app { 2 | display: grid; 3 | grid-template-rows: var(--app-header-height) auto; 4 | grid-template-columns: auto; 5 | grid-template-areas: 6 | "app-header" 7 | "page-layout"; 8 | } 9 | 10 | /* -- App Error ---------------------------------------------------------- */ 11 | 12 | .app-error { 13 | display: flex; 14 | flex: 1; 15 | flex-direction: column; 16 | align-items: center; 17 | padding-top: 8rem; 18 | font-weight: 600; 19 | color: var(--u-color_text); 20 | } 21 | 22 | .app-error .icon { 23 | font-size: 4rem; 24 | color: var(--u-color_critical_icon); 25 | } 26 | 27 | @import "./unison-share/banner.css"; 28 | @import "./unison-share/help-modal.css"; 29 | @import "./unison-share/publish-modal.css"; 30 | @import "./unison-share/report-bug-modal.css"; 31 | @import "./unison-share/download-modal.css"; 32 | @import "./unison-share/perspective-landing.css"; 33 | @import "./unison-share/page/catalog.css"; 34 | @import "./unison-share/page/user-page.css"; 35 | -------------------------------------------------------------------------------- /src/css/unison-share/banner.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | display: inline-flex; 3 | height: 1.5rem; 4 | border-radius: calc(1.5rem / 2); 5 | border: 1px solid var(--banner-border); 6 | background: var(--banner-bg); 7 | color: var(--banner-fg); 8 | padding: 0 0.75rem; 9 | line-height: 1; 10 | align-items: center; 11 | font-size: var(--font-size-small); 12 | } 13 | 14 | .banner:hover { 15 | text-decoration: none; 16 | transform: translate(0, 0.1rem); 17 | } 18 | 19 | /* Promotions */ 20 | 21 | .banner .banner-cta { 22 | color: var(--banner-fg-em); 23 | border-left: 1px solid var(--banner-border); 24 | display: inline-flex; 25 | height: 1.5rem; 26 | align-items: center; 27 | font-weight: bold; 28 | margin-left: 0.75rem; 29 | padding-left: 0.75rem; 30 | } 31 | 32 | .banner.article { 33 | --banner-fg: var(--color-green-4); 34 | --banner-bg: var(--color-gray-base); 35 | --banner-fg-em: var(--color-green-3); 36 | --banner-border: var(--color-gray-lighten-20); 37 | text-shadow: 0 1px rgba(0, 0, 0, 0.25); 38 | 39 | color: var(--banner-fg); 40 | } 41 | 42 | .banner.hacktoberfest { 43 | --banner-fg: var(--color-orange-3); 44 | --banner-bg: rgba(255, 136, 0, 0.2); /* color-orange-1 20% */ 45 | --banner-fg-em: var(--color-orange-5); 46 | --banner-border: var(--color-orange-1); 47 | text-shadow: 0 1px rgba(0, 0, 0, 0.25); 48 | } 49 | -------------------------------------------------------------------------------- /src/css/unison-share/download-modal.css: -------------------------------------------------------------------------------- 1 | #download-modal { 2 | width: 32rem; 3 | } 4 | 5 | #download-modal p { 6 | margin-bottom: 1.5rem; 7 | } 8 | 9 | #download-modal .hint { 10 | margin-top: 0.5rem; 11 | padding-left: calc(var(--border-radius-base) / 2); 12 | } 13 | -------------------------------------------------------------------------------- /src/css/unison-share/help-modal.css: -------------------------------------------------------------------------------- 1 | #help-modal .shortcuts { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 2.625rem; 5 | } 6 | 7 | #help-modal .shortcuts .shortcut-group { 8 | width: 20rem; 9 | display: flex; 10 | flex-direction: column; 11 | gap: 0.75rem; 12 | } 13 | 14 | #help-modal .shortcuts h3 { 15 | height: 1.5rem; 16 | margin-bottom: 1rem; 17 | display: flex; 18 | align-items: center; 19 | } 20 | 21 | #help-modal .shortcuts .row { 22 | display: flex; 23 | height: 1.5rem; 24 | align-items: center; 25 | } 26 | 27 | #help-modal .shortcuts .instructions { 28 | display: flex; 29 | flex-direction: row; 30 | justify-self: flex-end; 31 | margin-left: auto; 32 | } 33 | 34 | #help-modal .subtle { 35 | color: var(--color-modal-subtle-fg); 36 | } 37 | -------------------------------------------------------------------------------- /src/css/unison-share/page/user-page.css: -------------------------------------------------------------------------------- 1 | .user-page { 2 | --page-hero-height: 8rem; 3 | } 4 | 5 | .user-page .page-content { 6 | display: flex; 7 | align-items: flex-start; 8 | flex-direction: column; 9 | padding: 4rem 0; 10 | gap: 2rem; 11 | } 12 | 13 | .user-page .page-content .card { 14 | width: 100%; 15 | } 16 | 17 | .user-page .page-content .projects { 18 | display: flex; 19 | flex-direction: row; 20 | flex-wrap: wrap; 21 | gap: 0.5rem; 22 | } 23 | 24 | .user-page .page-content .projects .project-listing { 25 | width: calc(33%); 26 | margin-left: -0.25rem; 27 | } 28 | 29 | @media only screen and (max-width: 1025px) { 30 | .user-page { 31 | --page-hero-height: 4rem; 32 | } 33 | 34 | .user-page .page-content { 35 | padding: 2rem 0; 36 | } 37 | 38 | .user-page .page-content .card.contained { 39 | border: none; 40 | } 41 | 42 | .user-page .page-content .projects { 43 | flex-direction: column; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/css/unison-share/publish-modal.css: -------------------------------------------------------------------------------- 1 | #publish-modal { 2 | width: 32.5rem; 3 | } 4 | 5 | #publish-modal .main { 6 | font-size: var(--font-size-medium); 7 | } 8 | 9 | #publish-modal .help { 10 | display: inline-block; 11 | margin-top: 1rem; 12 | font-size: var(--font-size-small); 13 | color: var(--color-modal-subtle-fg-em); 14 | text-decoration: underline; 15 | } 16 | 17 | #publish-modal .help:hover { 18 | color: var(--color-modal-fg); 19 | } 20 | -------------------------------------------------------------------------------- /src/css/unison-share/report-bug-modal.css: -------------------------------------------------------------------------------- 1 | #report-bug-modal { 2 | width: 32.5rem; 3 | } 4 | 5 | #report-bug-modal .actions .button { 6 | margin-right: 0.5rem; 7 | } 8 | 9 | #report-bug-modal .actions .action { 10 | display: flex; 11 | align-items: center; 12 | margin-bottom: 1rem; 13 | } 14 | 15 | #report-bug-modal .actions .action strong { 16 | margin: 0 0.25rem; 17 | } 18 | 19 | #report-bug-modal .actions .action:last-child { 20 | margin-bottom: 0; 21 | } 22 | -------------------------------------------------------------------------------- /src/unisonLocal.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | Unison Local 15 | <% if (webpackConfig.mode === "production") {%> 16 | 26 | <% } %> 27 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/unisonLocal.js: -------------------------------------------------------------------------------- 1 | import "./css/ui.css"; 2 | import "./css/themes/unison-light.css"; 3 | import "./css/code.css"; 4 | import "./css/unison-local.css"; 5 | import "./UI/CopyOnClick"; // Include web components 6 | import detectOs from "./Lib/detectOs"; 7 | import preventDefaultGlobalKeyboardEvents from "./Lib/preventDefaultGlobalKeyboardEvents"; 8 | import { Elm } from "./UnisonLocal.elm"; 9 | 10 | console.log(` 11 | _____ _ 12 | | | |___|_|___ ___ ___ 13 | | | | | |_ -| . | | 14 | |_____|_|_|_|___|___|_|_| 15 | 16 | 17 | `); 18 | 19 | const basePath = new URL(document.baseURI).pathname; 20 | 21 | let apiBasePath; 22 | 23 | if (basePath === "/") { 24 | apiBasePath = ["api"]; 25 | } else { 26 | apiBasePath = basePath 27 | .replace("ui", "api") 28 | .split("/") 29 | .filter((p) => p !== ""); 30 | } 31 | 32 | const flags = { 33 | operatingSystem: detectOs(window.navigator), 34 | basePath, 35 | apiBasePath, 36 | }; 37 | 38 | preventDefaultGlobalKeyboardEvents(); 39 | 40 | // The main entry point for the `UnisonLocal` target of the Codebase UI. 41 | Elm.UnisonLocal.init({ flags }); 42 | -------------------------------------------------------------------------------- /src/unisonShare.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | 16 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Unison Share 33 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/unisonShare.js: -------------------------------------------------------------------------------- 1 | import "./css/ui.css"; 2 | import "./css/themes/unison-light.css"; 3 | import "./css/code.css"; 4 | import "./css/unison-share.css"; 5 | import "./UI/CopyOnClick"; // Include web components 6 | import detectOs from "./Lib/detectOs"; 7 | import preventDefaultGlobalKeyboardEvents from "./Lib/preventDefaultGlobalKeyboardEvents"; 8 | import { Elm } from "./UnisonShare.elm"; 9 | 10 | console.log(` 11 | _____ _ 12 | | | |___|_|___ ___ ___ 13 | | | | | |_ -| . | | 14 | |_____|_|_|_|___|___|_|_| 15 | 16 | 17 | `); 18 | 19 | const basePath = new URL(document.baseURI).pathname; 20 | const apiBasePath = ["api"]; 21 | 22 | const flags = { 23 | operatingSystem: detectOs(window.navigator), 24 | basePath, 25 | apiBasePath, 26 | }; 27 | 28 | preventDefaultGlobalKeyboardEvents(); 29 | 30 | // The main entry point for the `UnisonShare` target of the Codebase UI. 31 | Elm.UnisonShare.init({ flags }); 32 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/codebase-ui/02cf50b4b8d77325f4b17af8cc12da6b7221e5f4/static/favicon.ico -------------------------------------------------------------------------------- /tests/Code/CodebaseTree/NamespaceListingTests.elm: -------------------------------------------------------------------------------- 1 | module Code.CodebaseTree.NamespaceListingTests exposing (..) 2 | 3 | import Code.CodebaseTree.NamespaceListing as NamespaceListing exposing (NamespaceListing(..), NamespaceListingChild(..)) 4 | import Code.FullyQualifiedName as FQN 5 | import Code.Hash as Hash 6 | import Expect 7 | import RemoteData exposing (RemoteData(..)) 8 | import Test exposing (..) 9 | 10 | 11 | map : Test 12 | map = 13 | describe "CodebaseTree.NamespaceListing.map" 14 | [ test "runs the function on deeply nestes namespaces" <| 15 | \_ -> 16 | let 17 | hashA = 18 | Hash.fromString "#a" 19 | 20 | hashB = 21 | Hash.fromString "#c" 22 | 23 | hashC = 24 | Hash.fromString "#b" 25 | 26 | fqnA = 27 | FQN.fromString "a" 28 | 29 | fqnB = 30 | FQN.fromString "a.b" 31 | 32 | fqnC = 33 | FQN.fromString "a.b.c" 34 | 35 | original = 36 | Maybe.map3 37 | (\ha hb hc -> 38 | NamespaceListing ha 39 | fqnA 40 | (Success 41 | [ SubNamespace 42 | (NamespaceListing 43 | hb 44 | fqnB 45 | (Success [ SubNamespace (NamespaceListing hc fqnC NotAsked) ]) 46 | ) 47 | ] 48 | ) 49 | ) 50 | hashA 51 | hashB 52 | hashC 53 | 54 | expected = 55 | Maybe.map3 56 | (\ha hb hc -> 57 | NamespaceListing ha 58 | fqnA 59 | (Success 60 | [ SubNamespace 61 | (NamespaceListing 62 | hb 63 | fqnB 64 | (Success [ SubNamespace (NamespaceListing hc fqnC Loading) ]) 65 | ) 66 | ] 67 | ) 68 | ) 69 | hashA 70 | hashB 71 | hashC 72 | 73 | f ((NamespaceListing h fqn _) as nl) = 74 | if FQN.equals fqn fqnC then 75 | NamespaceListing h fqn Loading 76 | 77 | else 78 | nl 79 | 80 | result = 81 | Maybe.map (NamespaceListing.map f) original 82 | in 83 | Expect.equal expected result 84 | ] 85 | -------------------------------------------------------------------------------- /tests/Code/Definition/DocTests.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.DocTests exposing (..) 2 | 3 | import Code.Definition.Doc as Doc exposing (Doc(..)) 4 | import Expect 5 | import Lib.TreePath as TreePath 6 | import Test exposing (..) 7 | 8 | 9 | mergeWords : Test 10 | mergeWords = 11 | describe "Doc.mergeWords" 12 | [ test "merges adjacent Word elements with a separator" <| 13 | \_ -> 14 | let 15 | before = 16 | [ Word "Hello", Word "World", Blankline, Word "After", Word "non", Word "word" ] 17 | 18 | expected = 19 | [ Word "Hello World", Blankline, Word "After non word" ] 20 | in 21 | Expect.equal expected (Doc.mergeWords " " before) 22 | ] 23 | 24 | 25 | isDocFoldToggled : Test 26 | isDocFoldToggled = 27 | describe "Doc.isDocFoldToggled" 28 | [ test "returns True if the doc is toggled" <| 29 | \_ -> 30 | let 31 | toggles = 32 | Doc.toggleFold Doc.emptyDocFoldToggles id 33 | in 34 | Expect.true "doc is toggled" (Doc.isDocFoldToggled toggles id) 35 | , test "returns False if the doc is not toggled" <| 36 | \_ -> 37 | let 38 | toggles = 39 | Doc.emptyDocFoldToggles 40 | in 41 | Expect.false "doc is not toggled" (Doc.isDocFoldToggled toggles id) 42 | ] 43 | 44 | 45 | toString : Test 46 | toString = 47 | describe "Doc.toString" 48 | [ test "merges docs down to a string with a separator" <| 49 | \_ -> 50 | let 51 | before = 52 | Span [ Word "Hello", Word "World", Blankline, Word "After", Word "non", Word "word" ] 53 | 54 | expected = 55 | "Hello World After non word" 56 | in 57 | Expect.equal expected (Doc.toString " " before) 58 | ] 59 | 60 | 61 | toggleFold : Test 62 | toggleFold = 63 | describe "Doc.toggleFold" 64 | [ test "Adds a toggle if not present" <| 65 | \_ -> 66 | let 67 | toggles = 68 | Doc.toggleFold Doc.emptyDocFoldToggles id 69 | in 70 | Expect.true "doc was added" (Doc.isDocFoldToggled toggles id) 71 | , test "Removes a toggle if present" <| 72 | \_ -> 73 | let 74 | toggles = 75 | Doc.toggleFold Doc.emptyDocFoldToggles id 76 | 77 | without = 78 | Doc.toggleFold toggles id 79 | in 80 | Expect.false "doc was removed" (Doc.isDocFoldToggled without id) 81 | ] 82 | 83 | 84 | 85 | -- Helpers 86 | 87 | 88 | id : Doc.FoldId 89 | id = 90 | Doc.FoldId [ TreePath.VariantIndex 0, TreePath.ListIndex 3 ] 91 | -------------------------------------------------------------------------------- /tests/Code/Finder/SearchOptionsTests.elm: -------------------------------------------------------------------------------- 1 | module Code.Finder.SearchOptionsTests exposing (..) 2 | 3 | import Code.Finder.SearchOptions as SearchOptions exposing (SearchOptions(..), WithinOption(..)) 4 | import Code.FullyQualifiedName as FQN exposing (FQN) 5 | import Code.Hash as Hash 6 | import Code.Perspective as Perspective exposing (Perspective) 7 | import Expect 8 | import RemoteData exposing (RemoteData(..)) 9 | import Test exposing (..) 10 | 11 | 12 | init : Test 13 | init = 14 | describe "Finder.SearchOptions.init" 15 | [ test "with an FQN and the Root Perspective it returns the WithinNamespace WithinOption" <| 16 | \_ -> 17 | let 18 | result = 19 | codebasePerspective 20 | |> Maybe.map (\p -> SearchOptions.init p (Just namespaceFqn)) 21 | in 22 | Expect.equal (Just (SearchOptions (WithinNamespace namespaceFqn))) result 23 | , test "with an FQN and the Namespace Perspective it returns the WithinNamespace WithinOption" <| 24 | \_ -> 25 | let 26 | result = 27 | namespacePerspective 28 | |> Maybe.map (\p -> SearchOptions.init p (Just namespaceFqn)) 29 | in 30 | Expect.equal (Just (SearchOptions (WithinNamespace namespaceFqn))) result 31 | , test "without an FQN and the Root Perspective it returns the AllNamespaces WithinOption" <| 32 | \_ -> 33 | let 34 | result = 35 | codebasePerspective 36 | |> Maybe.map (\p -> SearchOptions.init p Nothing) 37 | in 38 | Expect.equal (Just (SearchOptions AllNamespaces)) result 39 | , test "without an FQN and the Namespace Perspective it returns the WithinNamespacePerspective WithinOption" <| 40 | \_ -> 41 | let 42 | result = 43 | namespacePerspective 44 | |> Maybe.map (\p -> SearchOptions.init p Nothing) 45 | in 46 | Expect.equal (Just (SearchOptions (WithinNamespacePerspective perspectiveFqn))) result 47 | ] 48 | 49 | 50 | removeWithin : Test 51 | removeWithin = 52 | describe "Finder.SearchOptions.removeWithin" 53 | [ test "when removing AllNamespaces it returns the AllNamespaces WithinOption" <| 54 | \_ -> 55 | let 56 | initial = 57 | SearchOptions AllNamespaces 58 | 59 | result = 60 | codebasePerspective 61 | |> Maybe.map (\p -> SearchOptions.removeWithin p initial) 62 | in 63 | Expect.equal (Just (SearchOptions AllNamespaces)) result 64 | , test "when removing WithinNamespacePerspective it returns the AllNamespaces WithinOption" <| 65 | \_ -> 66 | let 67 | initial = 68 | SearchOptions (SearchOptions.WithinNamespacePerspective perspectiveFqn) 69 | 70 | result = 71 | namespacePerspective 72 | |> Maybe.map (\p -> SearchOptions.removeWithin p initial) 73 | in 74 | Expect.equal (Just (SearchOptions AllNamespaces)) result 75 | , test "when removing WithinNamespace and Perspective is Root, it returns the AllNamespaces WithinOption" <| 76 | \_ -> 77 | let 78 | initial = 79 | SearchOptions (SearchOptions.WithinNamespace namespaceFqn) 80 | 81 | result = 82 | codebasePerspective 83 | |> Maybe.map (\p -> SearchOptions.removeWithin p initial) 84 | in 85 | Expect.equal (Just (SearchOptions AllNamespaces)) result 86 | , test "when removing WithinNamespace and Perspective is Namespace, it returns the WithinNamespacePerspective WithinOption" <| 87 | \_ -> 88 | let 89 | initial = 90 | SearchOptions (SearchOptions.WithinNamespace namespaceFqn) 91 | 92 | result = 93 | namespacePerspective 94 | |> Maybe.map (\p -> SearchOptions.removeWithin p initial) 95 | in 96 | Expect.equal (Just (SearchOptions (WithinNamespacePerspective perspectiveFqn))) result 97 | ] 98 | 99 | 100 | 101 | -- HELPERS 102 | 103 | 104 | namespaceFqn : FQN 105 | namespaceFqn = 106 | FQN.fromString "namespace.FQN" 107 | 108 | 109 | perspectiveFqn : FQN 110 | perspectiveFqn = 111 | FQN.fromString "perspective.FQN" 112 | 113 | 114 | namespacePerspective : Maybe Perspective 115 | namespacePerspective = 116 | Hash.fromString "#testhash" 117 | |> Maybe.map (\h -> Perspective.Namespace { rootHash = h, fqn = perspectiveFqn, details = NotAsked }) 118 | 119 | 120 | codebasePerspective : Maybe Perspective 121 | codebasePerspective = 122 | Hash.fromString "#testhash" 123 | |> Maybe.map Perspective.Root 124 | -------------------------------------------------------------------------------- /tests/Code/HashQualifiedTests.elm: -------------------------------------------------------------------------------- 1 | module Code.HashQualifiedTests exposing (..) 2 | 3 | import Code.FullyQualifiedName as FQN exposing (FQN) 4 | import Code.Hash as Hash exposing (Hash) 5 | import Code.HashQualified as HashQualified 6 | import Expect 7 | import Test exposing (..) 8 | 9 | 10 | name : Test 11 | name = 12 | describe "HashQualified.name" 13 | [ test "Returns Just name when NameOnly" <| 14 | \_ -> 15 | let 16 | hq = 17 | HashQualified.NameOnly name_ 18 | in 19 | Expect.equal (Just "test.name") (Maybe.map FQN.toString (HashQualified.name hq)) 20 | , test "Returns Nothing when HashOnly" <| 21 | \_ -> 22 | let 23 | hq = 24 | Maybe.map HashQualified.HashOnly hash_ 25 | in 26 | Expect.equal Nothing (Maybe.andThen HashQualified.name hq) 27 | , test "Returns Just name when HashQualified" <| 28 | \_ -> 29 | let 30 | hq = 31 | Maybe.map (HashQualified.HashQualified name_) hash_ 32 | in 33 | Expect.equal (Just "test.name") (Maybe.map FQN.toString (Maybe.andThen HashQualified.name hq)) 34 | ] 35 | 36 | 37 | hash : Test 38 | hash = 39 | describe "HashQualified.hash" 40 | [ test "Returns Nothing when NameOnly" <| 41 | \_ -> 42 | let 43 | hq = 44 | HashQualified.NameOnly name_ 45 | in 46 | Expect.equal Nothing (HashQualified.hash hq) 47 | , test "Returns Just hash when HashOnly" <| 48 | \_ -> 49 | let 50 | hq = 51 | Maybe.map HashQualified.HashOnly hash_ 52 | in 53 | Expect.equal (Just "#testhash") (Maybe.map Hash.toString (Maybe.andThen HashQualified.hash hq)) 54 | , test "Returns Just hash when HashQualified" <| 55 | \_ -> 56 | let 57 | hq = 58 | Maybe.map (HashQualified.HashQualified name_) hash_ 59 | in 60 | Expect.equal (Just "#testhash") (Maybe.map Hash.toString (Maybe.andThen HashQualified.hash hq)) 61 | ] 62 | 63 | 64 | fromUrlString : Test 65 | fromUrlString = 66 | describe "HashQualified.fromUrlString" 67 | [ test "HashOnly when called with a raw hash" <| 68 | \_ -> 69 | let 70 | expected = 71 | Maybe.map HashQualified.HashOnly hash_ 72 | in 73 | Expect.equal expected (Just (HashQualified.fromUrlString "@testhash")) 74 | , test "HashQualified when called with a name and hash" <| 75 | \_ -> 76 | let 77 | expected = 78 | Maybe.map (HashQualified.HashQualified urlName_) hash_ 79 | in 80 | Expect.equal expected (Just (HashQualified.fromUrlString "/test/;./name/@testhash")) 81 | , test "NameOnly when called with a name" <| 82 | \_ -> 83 | let 84 | expected = 85 | HashQualified.NameOnly urlName_ 86 | in 87 | Expect.equal expected (HashQualified.fromUrlString "test/;./name") 88 | ] 89 | 90 | 91 | 92 | -- TEST HELPERS 93 | 94 | 95 | name_ : FQN 96 | name_ = 97 | FQN.fromString "test.name" 98 | 99 | 100 | urlName_ : FQN 101 | urlName_ = 102 | FQN.fromString "test...name" 103 | 104 | 105 | hash_ : Maybe Hash 106 | hash_ = 107 | Hash.fromString "#testhash" 108 | -------------------------------------------------------------------------------- /tests/Code/ProjectTests.elm: -------------------------------------------------------------------------------- 1 | module Code.ProjectTests exposing (..) 2 | 3 | import Code.FullyQualifiedName as FQN 4 | import Code.Hash as Hash 5 | import Code.Project as Project 6 | import Expect 7 | import Test exposing (..) 8 | 9 | 10 | slug : Test 11 | slug = 12 | describe "Project.slug" 13 | [ test "Returns the slug of a project by owner and name" <| 14 | \_ -> 15 | Expect.equal 16 | "unison.http" 17 | (Project.slug project |> FQN.toString) 18 | ] 19 | 20 | 21 | 22 | -- Helpers 23 | 24 | 25 | project : Project.ProjectListing 26 | project = 27 | { owner = Project.Owner "unison" 28 | , name = FQN.fromString "http" 29 | , hash = Hash.unsafeFromString "##unison.http" 30 | } 31 | -------------------------------------------------------------------------------- /tests/Lib/SearchResultsTests.elm: -------------------------------------------------------------------------------- 1 | module Lib.SearchResultsTests exposing (..) 2 | 3 | import Expect 4 | import Lib.SearchResults as SearchResults 5 | import Test exposing (..) 6 | 7 | 8 | fromList : Test 9 | fromList = 10 | describe "SearchResults.fromList" 11 | [ test "Returns Empty for empty list" <| 12 | \_ -> 13 | let 14 | result = 15 | SearchResults.fromList [] 16 | in 17 | Expect.equal SearchResults.Empty result 18 | , test "Includes all matches" <| 19 | \_ -> 20 | let 21 | result = 22 | SearchResults.fromList [ "a", "b", "c" ] 23 | |> SearchResults.toList 24 | in 25 | Expect.equal [ "a", "b", "c" ] result 26 | ] 27 | 28 | 29 | next : Test 30 | next = 31 | describe "SearchResults.next" 32 | [ test "moves focus to the next element" <| 33 | \_ -> 34 | let 35 | result = 36 | SearchResults.from [ "a" ] "b" [ "c" ] 37 | |> SearchResults.next 38 | |> SearchResults.toMaybe 39 | |> Maybe.map SearchResults.focus 40 | in 41 | Expect.equal (Just "c") result 42 | , test "keeps focus if no elements after" <| 43 | \_ -> 44 | let 45 | result = 46 | SearchResults.from [ "a", "b" ] "c" [] 47 | |> SearchResults.next 48 | |> SearchResults.toMaybe 49 | |> Maybe.map SearchResults.focus 50 | in 51 | Expect.equal (Just "c") result 52 | ] 53 | 54 | 55 | prev : Test 56 | prev = 57 | describe "SearchResults.prev" 58 | [ test "moves focus to the prev element" <| 59 | \_ -> 60 | let 61 | result = 62 | SearchResults.from [ "a" ] "b" [ "c" ] 63 | |> SearchResults.prev 64 | |> SearchResults.toMaybe 65 | |> Maybe.map SearchResults.focus 66 | in 67 | Expect.equal (Just "a") result 68 | , test "keeps focus if no elements before" <| 69 | \_ -> 70 | let 71 | result = 72 | SearchResults.from [] "a" [ "b", "c" ] 73 | |> SearchResults.prev 74 | |> SearchResults.toMaybe 75 | |> Maybe.map SearchResults.focus 76 | in 77 | Expect.equal (Just "a") result 78 | ] 79 | 80 | 81 | 82 | -- QUERY 83 | 84 | 85 | getAt : Test 86 | getAt = 87 | describe "SearchResults.getAt" 88 | [ test "When there are no results it returns Nothing" <| 89 | \_ -> 90 | let 91 | result = 92 | SearchResults.fromList [] |> SearchResults.getAt 3 93 | in 94 | Expect.equal Nothing result 95 | , test "When there the index is out of bounds it returns Nothing" <| 96 | \_ -> 97 | let 98 | result = 99 | SearchResults.fromList [ "foo", "bar", "baz" ] |> SearchResults.getAt 10 100 | in 101 | Expect.equal Nothing result 102 | , test "When there is an item at the index it returns Just of that item" <| 103 | \_ -> 104 | let 105 | result = 106 | SearchResults.fromList [ "foo", "bar", "baz" ] |> SearchResults.getAt 1 107 | in 108 | Expect.equal (Just "bar") result 109 | ] 110 | 111 | 112 | 113 | -- MAP 114 | 115 | 116 | mapToList : Test 117 | mapToList = 118 | describe "SearchResults.mapToList" 119 | [ test "Maps definitions" <| 120 | \_ -> 121 | let 122 | result = 123 | SearchResults.from [ "a" ] "b" [ "c" ] 124 | |> SearchResults.mapToList (\x isFocused -> ( x ++ "mapped", isFocused )) 125 | 126 | expected = 127 | [ ( "amapped", False ), ( "bmapped", True ), ( "cmapped", False ) ] 128 | in 129 | Expect.equal expected result 130 | ] 131 | -------------------------------------------------------------------------------- /tests/Lib/TreePathTests.elm: -------------------------------------------------------------------------------- 1 | module Lib.TreePathTests exposing (..) 2 | 3 | import Expect 4 | import Lib.TreePath as TreePath 5 | import Test exposing (..) 6 | 7 | 8 | toString : Test 9 | toString = 10 | describe "TreePath.toString" 11 | [ test "Returns a string version of a TreePath" <| 12 | \_ -> 13 | let 14 | path = 15 | [ TreePath.VariantIndex 0, TreePath.ListIndex 1, TreePath.VariantIndex 4 ] 16 | in 17 | Expect.equal "VariantIndex#0.ListIndex#1.VariantIndex#4" (TreePath.toString path) 18 | ] 19 | -------------------------------------------------------------------------------- /tests/UnisonShare/Catalog/CatalogMaskTests.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.Catalog.CatalogMaskTests exposing (..) 2 | 3 | import Code.Definition.Doc as Doc 4 | import Expect 5 | import Test exposing (..) 6 | import UnisonShare.Catalog.CatalogMask as CatalogMask 7 | 8 | 9 | fromDoc : Test 10 | fromDoc = 11 | describe "CatalogMask.fromDoc" 12 | [ test "Creates a CatalogMask from a Doc" <| 13 | \_ -> 14 | Expect.equal rawMask 15 | (CatalogMask.fromDoc doc |> CatalogMask.toList) 16 | ] 17 | 18 | 19 | fromList : Test 20 | fromList = 21 | describe "CatalogMask.fromList" 22 | [ test "Creates a CatalogMask from a List" <| 23 | \_ -> 24 | let 25 | catalogMask = 26 | CatalogMask.fromList rawMask 27 | in 28 | Expect.equal rawMask 29 | (CatalogMask.toList catalogMask) 30 | ] 31 | 32 | 33 | categoryOf : Test 34 | categoryOf = 35 | describe "CatalogMask.categoryOf" 36 | [ test "given a project, returns its category " <| 37 | \_ -> 38 | let 39 | catalogMask = 40 | CatalogMask.fromList rawMask 41 | in 42 | Expect.equal (Just "Featured") (CatalogMask.categoryOf "unison.base" catalogMask) 43 | ] 44 | 45 | 46 | categories : Test 47 | categories = 48 | describe "CatalogMask.categories" 49 | [ test "Returns all categories in a Mask" <| 50 | \_ -> 51 | let 52 | catalogMask = 53 | CatalogMask.fromList rawMask 54 | in 55 | Expect.equal 56 | [ "Featured" 57 | , "Web & Networking" 58 | , "Parsers & Text Manipulation" 59 | , "Datatypes" 60 | ] 61 | (CatalogMask.categories catalogMask) 62 | ] 63 | 64 | 65 | projectNames : Test 66 | projectNames = 67 | describe "CatalogMask.projectNames" 68 | [ test "Returns all project names in a Mask" <| 69 | \_ -> 70 | let 71 | catalogMask = 72 | CatalogMask.fromList rawMask 73 | in 74 | Expect.equal 75 | [ "unison.base" 76 | , "unison.distributed" 77 | , "unison.http" 78 | , "hojberg.textExtra" 79 | , "hojberg.nanoid" 80 | ] 81 | (CatalogMask.projectNames catalogMask) 82 | ] 83 | 84 | 85 | 86 | -- HELPERS 87 | 88 | 89 | rawMask : List ( String, String ) 90 | rawMask = 91 | [ ( "unison.base", "Featured" ) 92 | , ( "unison.distributed", "Featured" ) 93 | , ( "unison.http", "Web & Networking" ) 94 | , ( "hojberg.textExtra", "Parsers & Text Manipulation" ) 95 | , ( "hojberg.nanoid", "Datatypes" ) 96 | ] 97 | 98 | 99 | doc : Doc.Doc 100 | doc = 101 | Doc.UntitledSection 102 | [ Doc.Section (Doc.Word "Featured") 103 | [ Doc.BulletedList [ Doc.Word "unison.base", Doc.Word "unison.distributed" ] ] 104 | , Doc.Section (Doc.Word "Web & Networking") 105 | [ Doc.BulletedList [ Doc.Word "unison.http" ] ] 106 | , Doc.Section (Doc.Word "Parsers & Text Manipulation") 107 | [ Doc.BulletedList [ Doc.Word "hojberg.textExtra" ] ] 108 | , Doc.Section (Doc.Word "Datatypes") 109 | [ Doc.BulletedList [ Doc.Word "hojberg.nanoid" ] ] 110 | ] 111 | -------------------------------------------------------------------------------- /tests/UnisonShare/CatalogTests.elm: -------------------------------------------------------------------------------- 1 | module UnisonShare.CatalogTests exposing (..) 2 | 3 | import Code.FullyQualifiedName as FQN 4 | import Code.Hash as Hash 5 | import Code.Project as Project 6 | import Expect 7 | import Test exposing (..) 8 | import UnisonShare.Catalog as Catalog 9 | import UnisonShare.Catalog.CatalogMask as CatalogMask 10 | 11 | 12 | catalog : Test 13 | catalog = 14 | describe "Catalog.catalog" 15 | [ describe "Create a Catalog from a CatalogMask and ProjectListings" 16 | [ test "It retains only the overlapping items between the mask and the listings" <| 17 | \_ -> 18 | let 19 | projectListings_ = 20 | [ baseListing, distributedListing, textExtraListing, nanoidListing ] 21 | 22 | catalog_ = 23 | Catalog.catalog catalogMask projectListings_ 24 | in 25 | Expect.equal 26 | [ ( "Featured", [ baseListing, distributedListing ] ) 27 | , ( "Parsers & Text Manipulation", [ textExtraListing ] ) 28 | ] 29 | (Catalog.toList catalog_) 30 | ] 31 | ] 32 | 33 | 34 | categories : Test 35 | categories = 36 | describe "Catalog.categories" 37 | [ test "Extracts the categories of a Catalog that exist in both the mask and the project listings" <| 38 | \_ -> 39 | let 40 | projectListings_ = 41 | [ baseListing, distributedListing, textExtraListing ] 42 | 43 | catalog_ = 44 | Catalog.catalog catalogMask projectListings_ 45 | in 46 | Expect.equal [ "Featured", "Parsers & Text Manipulation" ] (Catalog.categories catalog_) 47 | ] 48 | 49 | 50 | projectListings : Test 51 | projectListings = 52 | describe "Catalog.projectListings" 53 | [ test "Extracts the projectListings of a Catalog that exist in both the mask and the project listings" <| 54 | \_ -> 55 | let 56 | projectListings_ = 57 | [ baseListing, textExtraListing, nanoidListing ] 58 | 59 | catalog_ = 60 | Catalog.catalog catalogMask projectListings_ 61 | in 62 | Expect.equal [ baseListing, textExtraListing ] (Catalog.projectListings catalog_) 63 | ] 64 | 65 | 66 | toList : Test 67 | toList = 68 | describe "Catalog.toList" 69 | [ test "Returns the Catalog as a List" <| 70 | \_ -> 71 | let 72 | projectListings_ = 73 | [ baseListing, distributedListing, textExtraListing, nanoidListing ] 74 | 75 | catalog_ = 76 | Catalog.catalog catalogMask projectListings_ 77 | in 78 | Expect.equal 79 | [ ( "Featured", [ baseListing, distributedListing ] ) 80 | , ( "Parsers & Text Manipulation", [ textExtraListing ] ) 81 | ] 82 | (Catalog.toList catalog_) 83 | ] 84 | 85 | 86 | 87 | -- helpers 88 | 89 | 90 | catalogMask : CatalogMask.CatalogMask 91 | catalogMask = 92 | CatalogMask.fromList 93 | [ ( "unison.base", "Featured" ) 94 | , ( "unison.distributed", "Featured" ) 95 | , ( "unison.http", "Web & Networking" ) 96 | , ( "hojberg.textExtra", "Parsers & Text Manipulation" ) 97 | , ( "hojberg.money", "Datatypes" ) 98 | ] 99 | 100 | 101 | baseListing : Project.ProjectListing 102 | baseListing = 103 | { owner = Project.Owner "unison" 104 | , name = FQN.fromString "base" 105 | , hash = Hash.unsafeFromString "##unison.base" 106 | } 107 | 108 | 109 | distributedListing : Project.ProjectListing 110 | distributedListing = 111 | { owner = Project.Owner "unison" 112 | , name = FQN.fromString "distributed" 113 | , hash = Hash.unsafeFromString "##unison.distributed" 114 | } 115 | 116 | 117 | httpListing : Project.ProjectListing 118 | httpListing = 119 | { owner = Project.Owner "unison" 120 | , name = FQN.fromString "http" 121 | , hash = Hash.unsafeFromString "##unison.http" 122 | } 123 | 124 | 125 | textExtraListing : Project.ProjectListing 126 | textExtraListing = 127 | { owner = Project.Owner "hojberg" 128 | , name = FQN.fromString "textExtra" 129 | , hash = Hash.unsafeFromString "##hojberg.textExtra" 130 | } 131 | 132 | 133 | {-| Note: does purposely not exist in mask 134 | -} 135 | nanoidListing : Project.ProjectListing 136 | nanoidListing = 137 | { owner = Project.Owner "hojberg" 138 | , name = FQN.fromString "nanoid" 139 | , hash = Hash.unsafeFromString "##hojberg.nanoid" 140 | } 141 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const FileManagerPlugin = require("filemanager-webpack-plugin"); 4 | const CopyPlugin = require("copy-webpack-plugin"); 5 | 6 | const shared = { 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.css$/i, 11 | use: ["style-loader", "css-loader"], 12 | }, 13 | { 14 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 15 | type: "asset/resource", 16 | }, 17 | { 18 | test: /\.(woff(2)?|ttf|eot)$/i, 19 | type: "asset/resource", 20 | }, 21 | { 22 | test: /\.elm$/, 23 | exclude: [/elm-stuff/, /node_modules/], 24 | use: [ 25 | { 26 | loader: "elm-asset-webpack-loader", 27 | }, 28 | { 29 | loader: "elm-webpack-loader", 30 | options: { 31 | debug: false, 32 | cwd: __dirname, 33 | }, 34 | }, 35 | ], 36 | }, 37 | ], 38 | }, 39 | resolve: { 40 | alias: { 41 | assets: path.resolve(__dirname, "src/assets/"), 42 | }, 43 | }, 44 | }; 45 | 46 | const unisonShareCfg = { 47 | ...shared, 48 | 49 | entry: "./src/unisonShare.js", 50 | 51 | plugins: [ 52 | new HtmlWebpackPlugin({ 53 | favicon: "./static/favicon.ico", 54 | template: "./src/unisonShare.ejs", 55 | inject: "body", 56 | publicPath: "/static/", 57 | base: "/", 58 | filename: path.resolve(__dirname, "dist/unisonShare/index.html"), 59 | }), 60 | 61 | new CopyPlugin({ 62 | patterns: [ 63 | { 64 | from: "src/assets/unison-share-social.png", 65 | to: "unison-share-social.png", 66 | }, 67 | ], 68 | }), 69 | 70 | new FileManagerPlugin({ 71 | events: { 72 | onEnd: { 73 | archive: [ 74 | { source: "dist/unisonShare", destination: "dist/unisonShare.zip" }, 75 | ], 76 | }, 77 | }, 78 | }), 79 | ], 80 | 81 | output: { 82 | filename: "[name].[contenthash].js", 83 | path: path.resolve(__dirname, "dist/unisonShare/static"), 84 | clean: true, 85 | }, 86 | }; 87 | 88 | const unisonLocalCfg = { 89 | ...shared, 90 | 91 | entry: "./src/unisonLocal.js", 92 | 93 | plugins: [ 94 | new HtmlWebpackPlugin({ 95 | favicon: "./static/favicon.ico", 96 | template: "./src/unisonLocal.ejs", 97 | inject: "body", 98 | publicPath: "/static/", 99 | base: false, // set dynamically by grabbing the 2 first path segments in the url. 100 | filename: path.resolve(__dirname, "dist/unisonLocal/index.html"), 101 | }), 102 | 103 | new FileManagerPlugin({ 104 | events: { 105 | onEnd: { 106 | archive: [ 107 | { source: "dist/unisonLocal", destination: "dist/unisonLocal.zip" }, 108 | ], 109 | }, 110 | }, 111 | }), 112 | ], 113 | 114 | output: { 115 | filename: "[name].[contenthash].js", 116 | path: path.resolve(__dirname, "dist/unisonLocal/static"), 117 | clean: true, 118 | }, 119 | }; 120 | 121 | module.exports = [unisonShareCfg, unisonLocalCfg]; 122 | -------------------------------------------------------------------------------- /webpack.unisonLocal.dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | const API_URL = process.env.API_URL || "127.0.0.1:8080"; 5 | 6 | module.exports = { 7 | entry: "./src/unisonLocal.js", 8 | 9 | resolve: { 10 | alias: { 11 | assets: path.resolve(__dirname, "src/assets/"), 12 | }, 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.css$/i, 19 | use: ["style-loader", "css-loader"], 20 | }, 21 | { 22 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 23 | type: "asset/resource", 24 | }, 25 | { 26 | test: /\.(woff(2)?|ttf|eot)$/i, 27 | type: "asset/resource", 28 | }, 29 | { 30 | test: /\.elm$/, 31 | exclude: [/elm-stuff/, /node_modules/], 32 | use: [ 33 | { 34 | loader: "elm-asset-webpack-loader", 35 | }, 36 | { 37 | loader: "elm-webpack-loader", 38 | options: { 39 | debug: false, 40 | cwd: __dirname, 41 | }, 42 | }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | 48 | plugins: [ 49 | new HtmlWebpackPlugin({ 50 | favicon: "./static/favicon.ico", 51 | template: "./src/unisonLocal.ejs", 52 | inject: "body", 53 | publicPath: "/", 54 | base: "/", 55 | filename: path.resolve(__dirname, "dist/dev/index.html"), 56 | }), 57 | ], 58 | 59 | output: { 60 | filename: "[name].[contenthash].js", 61 | path: path.resolve(__dirname, "dist/dev"), 62 | clean: true, 63 | }, 64 | 65 | devServer: { 66 | historyApiFallback: { 67 | disableDotRule: true, 68 | }, 69 | proxy: { 70 | "/api": { 71 | target: API_URL, 72 | pathRewrite: { "^/api": "" }, 73 | logLevel: "debug", 74 | }, 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /webpack.unisonShare.dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | const API_URL = process.env.API_URL || "127.0.0.1:8080"; 5 | 6 | module.exports = { 7 | entry: "./src/unisonShare.js", 8 | 9 | resolve: { 10 | alias: { 11 | assets: path.resolve(__dirname, "src/assets/"), 12 | }, 13 | }, 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.css$/i, 19 | use: ["style-loader", "css-loader"], 20 | }, 21 | { 22 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 23 | type: "asset/resource", 24 | }, 25 | { 26 | test: /\.(woff(2)?|ttf|eot)$/i, 27 | type: "asset/resource", 28 | }, 29 | { 30 | test: /\.elm$/, 31 | exclude: [/elm-stuff/, /node_modules/], 32 | use: [ 33 | { 34 | loader: "elm-asset-webpack-loader", 35 | }, 36 | { 37 | loader: "elm-webpack-loader", 38 | options: { 39 | debug: false, 40 | cwd: __dirname, 41 | }, 42 | }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | 48 | plugins: [ 49 | new HtmlWebpackPlugin({ 50 | favicon: "./static/favicon.ico", 51 | template: "./src/unisonShare.ejs", 52 | inject: "body", 53 | publicPath: "/", 54 | base: "/", 55 | filename: path.resolve(__dirname, "dist/dev/index.html"), 56 | }), 57 | ], 58 | 59 | output: { 60 | filename: "[name].[contenthash].js", 61 | path: path.resolve(__dirname, "dist/dev"), 62 | clean: true, 63 | }, 64 | 65 | devServer: { 66 | historyApiFallback: { 67 | disableDotRule: true, 68 | }, 69 | proxy: { 70 | "/api": { 71 | target: API_URL, 72 | pathRewrite: { "^/api": "" }, 73 | logLevel: "debug", 74 | }, 75 | }, 76 | }, 77 | }; 78 | --------------------------------------------------------------------------------