├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── elm-watch.json ├── elm.json ├── examples ├── elm.json └── src │ └── Main.elm ├── package-lock.json ├── package.json ├── review ├── elm.json └── src │ └── ReviewConfig.elm ├── scripts └── check-css-vars.mjs ├── src ├── Code │ ├── Branch.elm │ ├── BranchRef.elm │ ├── Browser.elm │ ├── 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 │ ├── DefinitionDetailTooltip.elm │ ├── DefinitionSummaryTooltip.elm │ ├── EmptyState.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 │ ├── ProjectDependency.elm │ ├── ProjectName.elm │ ├── ProjectNameListing.elm │ ├── ProjectSlug.elm │ ├── README.md │ ├── ReadmeCard.elm │ ├── Source │ │ └── SourceViewConfig.elm │ ├── Syntax.elm │ ├── Syntax │ │ ├── SyntaxConfig.elm │ │ ├── SyntaxSegment.elm │ │ └── SyntaxSegmentHelp.elm │ ├── UrlParsers.elm │ ├── Version.elm │ ├── Workspace.elm │ └── Workspace │ │ ├── WorkspaceItem.elm │ │ ├── WorkspaceItems.elm │ │ ├── WorkspaceMinimap.elm │ │ └── Zoom.elm ├── Lib │ ├── Aria.elm │ ├── Color │ │ └── Harmony.elm │ ├── Decode │ │ └── Helpers.elm │ ├── EmbedKatex.elm │ ├── EmbedKatex.js │ ├── EmbedSvg.elm │ ├── EmbedSvg.js │ ├── HttpApi.elm │ ├── MermaidDiagram.elm │ ├── MermaidDiagram.js │ ├── OnClickOutside.elm │ ├── OnClickOutside.js │ ├── OperatingSystem.elm │ ├── Paginated.elm │ ├── README.md │ ├── ScrollEvent.elm │ ├── ScrollTo.elm │ ├── Search.elm │ ├── SearchResults.elm │ ├── Slug.elm │ ├── String │ │ └── Helpers.elm │ ├── TreePath.elm │ ├── UnicodeSort.elm │ ├── UnicodeSort │ │ └── UnicodeData.elm │ ├── UserHandle.elm │ ├── Util.elm │ ├── detectOs.d.ts │ ├── detectOs.js │ └── preventDefaultGlobalKeyboardEvents.js ├── UI.elm ├── UI │ ├── ActionMenu.elm │ ├── AnchoredOverlay.elm │ ├── AppDocument.elm │ ├── AppHeader.elm │ ├── Avatar.elm │ ├── AvatarStack.elm │ ├── Banner.elm │ ├── Button.elm │ ├── ByAt.elm │ ├── Card.elm │ ├── Click.elm │ ├── Color.elm │ ├── CopyField.elm │ ├── CopyOnClick.elm │ ├── CopyOnClick.js │ ├── CopyrightYear.elm │ ├── CopyrightYear.js │ ├── DateTime.elm │ ├── Divider.elm │ ├── EmptyState.elm │ ├── EmptyStateCard.elm │ ├── ErrorCard.elm │ ├── ExternalLinkIcon.elm │ ├── FoldToggle.elm │ ├── Form │ │ ├── Checkbox.elm │ │ ├── CheckboxField.elm │ │ ├── RadioField.elm │ │ └── TextField.elm │ ├── Icon.elm │ ├── KeyboardShortcut.elm │ ├── KeyboardShortcut │ │ ├── Key.elm │ │ └── KeyboardEvent.elm │ ├── KpiTag.elm │ ├── MillerColumns.elm │ ├── Modal.elm │ ├── ModalOverlay.elm │ ├── ModalOverlay.js │ ├── Navigation.elm │ ├── Nudge.elm │ ├── OnSurface.elm │ ├── PageContent.elm │ ├── PageHeader.elm │ ├── PageLayout.elm │ ├── PageTitle.elm │ ├── Placeholder.elm │ ├── ProfileSnippet.elm │ ├── README.md │ ├── Sidebar.elm │ ├── Sizing.elm │ ├── StatusBanner.elm │ ├── StatusIndicator.elm │ ├── StatusMessage.elm │ ├── Steps.elm │ ├── TabList.elm │ ├── Tag.elm │ ├── Toolbar.elm │ └── Tooltip.elm └── css │ ├── code.css │ ├── code │ ├── definition-doc.css │ ├── empty-state-bg.svg │ ├── empty-state.css │ ├── finder.css │ ├── fully-qualified-name.css │ ├── hash.css │ ├── hashvatar.css │ ├── project-name-listing.css │ ├── project-name.css │ ├── syntax.css │ ├── version.css │ ├── workspace-item.css │ ├── workspace-minimap.css │ └── workspace.css │ ├── themes │ └── unison-light.css │ ├── ui.css │ └── ui │ ├── animations.css │ ├── base.css │ ├── colors.css │ ├── components.css │ ├── components │ ├── action-menu.css │ ├── anchored-overlay.css │ ├── app-header.css │ ├── avatar-stack.css │ ├── avatar.css │ ├── badges.css │ ├── button.css │ ├── by-at.css │ ├── card.css │ ├── codebase-tree.css │ ├── copy-field.css │ ├── copy-on-click.css │ ├── date-time.css │ ├── divider.css │ ├── empty-state-card.css │ ├── empty-state.css │ ├── error-card.css │ ├── external-link-icon.css │ ├── fold-toggle.css │ ├── form.css │ ├── icon.css │ ├── keyboard-shortcuts.css │ ├── kpi-tag.css │ ├── loading-placeholder.css │ ├── modal.css │ ├── navigation.css │ ├── nudge.css │ ├── page-header.css │ ├── placeholder.css │ ├── profile-snippet.css │ ├── sidebar.css │ ├── status-banner.css │ ├── status-indicator.css │ ├── status-message.css │ ├── steps.css │ ├── tab-list.css │ ├── tag.css │ ├── text.css │ ├── toolbar.css │ └── tooltip.css │ ├── core.css │ ├── empty-state_grid.svg │ ├── empty-state_icon-cloud.svg │ ├── empty-state_search_on-dark.svg │ ├── empty-state_search_on-light.svg │ ├── fonts.css │ ├── fonts │ ├── FiraCode │ │ ├── FiraCode-Bold.woff2 │ │ ├── FiraCode-Light.woff2 │ │ ├── FiraCode-Medium.woff2 │ │ ├── FiraCode-Regular.woff2 │ │ ├── FiraCode-SemiBold.woff2 │ │ └── FiraCode-VF.woff2 │ └── Inter │ │ ├── Inter-italic.var.woff2 │ │ ├── Inter-roman.var.woff2 │ │ ├── Inter.var.woff2 │ │ └── LICENSE.txt │ ├── page-layout.css │ └── viewport.css ├── storybook ├── .babelrc.json ├── .storybook │ ├── main.js │ ├── preview-head.html │ ├── preview.js │ └── webpack.config.js ├── README.md ├── doc │ ├── UICore-Storybook-Button.png │ ├── UICore-Storybook-Icon.png │ └── UICore-Storybook-WorkspaceItem.png ├── elm.json ├── package-lock.json ├── package.json ├── static │ ├── base_readme.json │ ├── blog_def.json │ ├── cloud_config_def.json │ ├── increment_term_def.json │ ├── long.json │ ├── nat_gt_term_def.json │ ├── positive_int_2.json │ └── volturno_checkpointing_def.json └── stories │ ├── Helpers │ ├── Layout.elm │ ├── ReferenceHelper.elm │ └── style.css │ ├── Stories │ ├── Code │ │ ├── PageContent.elm │ │ ├── Workspace.elm │ │ ├── WorkspaceItem.elm │ │ ├── WorkspaceMinimap.elm │ │ └── code.stories.js │ ├── Lib │ │ ├── MermaidDiagram.elm │ │ └── lib.stories.js │ └── UI │ │ ├── Banner.elm │ │ ├── Button.elm │ │ ├── Card.elm │ │ ├── CopyField.elm │ │ ├── ErrorCard.elm │ │ ├── FoldToggle.elm │ │ ├── Form │ │ └── TextField.elm │ │ ├── Icon.elm │ │ ├── KeyboardShortcut.elm │ │ ├── Modal.elm │ │ ├── Navigation.elm │ │ ├── PageHeader.elm │ │ ├── Placeholder.elm │ │ ├── StatusBanner.elm │ │ ├── Tooltip.elm │ │ └── ui.stories.js │ └── initElmStory.js └── tests ├── Code ├── BranchRefTests.elm ├── CodebaseTree │ └── NamespaceListingTests.elm ├── Definition │ ├── DocTests.elm │ └── InfoTests.elm ├── Finder │ └── SearchOptionsTests.elm ├── FullyQualifiedNameTests.elm ├── HashQualifiedTests.elm ├── HashTests.elm ├── ProjectDependencyTests.elm ├── ProjectNameTests.elm ├── VersionTests.elm └── Workspace │ └── WorkspaceItemsTests.elm ├── Lib ├── HttpApiTests.elm ├── SearchResultsTests.elm ├── SlugTests.elm ├── TreePathTests.elm └── UserHandleTests.elm └── UI └── DateTimeTests.elm /.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 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm ci 25 | - run: npm run check-css-vars 26 | - run: npm run build 27 | - run: npm test 28 | - run: npm run review 29 | - run: npx prettier --check . 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | .DS_Store 3 | public/bundle.js 4 | node_modules 5 | build 6 | .unisonHistory 7 | .unison 8 | storybook/storybook-static 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | public/bundle.js 3 | node_modules 4 | dist 5 | build 6 | .unison 7 | # apparently prettier adds a newline to all files and this clashes with 8 | # autogenerated Elm JSON files: 9 | # https://github.com/prettier/prettier/issues/6360 10 | elm-git.json 11 | elm.json 12 | review/suppressed/*.json 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-2023, 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 UI Core 2 | 3 | Shared design system and core components for Unison Local and Unison Share. 4 | 5 | [![CI](https://github.com/unisonweb/ui-core/actions/workflows/ci.yml/badge.svg)](https://github.com/unisonweb/ui-core/actions/workflows/ci.yml) 6 | 7 | ![Alt](https://repobeats.axiom.co/api/embed/230665134b57af8df5cc834eccd05fc3cc614fec.svg "Repobeats analytics image") 8 | 9 | ## Adding new Icons 10 | 11 | To add new icons, copy the SVG markup to a new function in `/src/UI/Icon.elm` 12 | following the convention seen there of other icons. The color (`fill` or 13 | `stroke`) of the shape must be 14 | [`currentColor`](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/color). 15 | 16 | ## Community 17 | 18 | [Code of conduct](https://www.unisonweb.org/code-of-conduct/) 19 | -------------------------------------------------------------------------------- /examples/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": ["src", "../src"], 4 | "elm-version": "0.19.1", 5 | "dependencies": { 6 | "direct": { 7 | "elm/browser": "1.0.2", 8 | "elm/core": "1.0.5", 9 | "elm/html": "1.0.0" 10 | }, 11 | "indirect": { 12 | "elm/json": "1.1.3", 13 | "elm/time": "1.0.0", 14 | "elm/url": "1.0.0", 15 | "elm/virtual-dom": "1.0.2" 16 | } 17 | }, 18 | "test-dependencies": { 19 | "direct": {}, 20 | "indirect": {} 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (Msg(..), main, update, view) 2 | 3 | import Browser 4 | import Html exposing (Html, button, div, text) 5 | import Html.Events exposing (onClick) 6 | 7 | 8 | main = 9 | Browser.sandbox { init = 0, update = update, view = view } 10 | 11 | 12 | type Msg 13 | = Increment 14 | | Decrement 15 | 16 | 17 | update msg model = 18 | case msg of 19 | Increment -> 20 | model + 1 21 | 22 | Decrement -> 23 | model - 1 24 | 25 | 26 | view model = 27 | div [] 28 | [ button [ onClick Decrement ] [ text "-" ] 29 | , div [] [ text (String.fromInt model) ] 30 | , button [ onClick Increment ] [ text "+" ] 31 | ] 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unison/ui-core", 3 | "description": "Shared design system and core components for Unison Local and Unison Share", 4 | "scripts": { 5 | "test": "elm-test", 6 | "review": "elm-review", 7 | "build": "elm make", 8 | "check-css-vars": "scripts/check-css-vars.mjs", 9 | "watch": "elm-watch hot", 10 | "check": "npm run build; npm run review; npm run test; npm run check-css-vars; prettier --check .", 11 | "sha": "git rev-parse HEAD | cut -c 1-8" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/unisonweb/ui-core.git" 16 | }, 17 | "author": "Unison Computing", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/unisonweb/ui-core/issues" 21 | }, 22 | "homepage": "https://github.com/unisonweb/ui-core#readme", 23 | "devDependencies": { 24 | "elm": "^0.19.1-5", 25 | "elm-format": "^0.8.7", 26 | "elm-review": "^2.13.2", 27 | "elm-test": "^0.19.1-revision15", 28 | "elm-watch": "^1.2.2", 29 | "glob": "^11.0.1", 30 | "prettier": "^3.5.3" 31 | }, 32 | "dependencies": { 33 | "@oddbird/css-anchor-positioning": "^0.4.0", 34 | "katex": "^0.16.4", 35 | "mermaid": "^11.4.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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.15.1", 11 | "jfmengels/elm-review-simplify": "2.1.6", 12 | "jfmengels/elm-review-unused": "1.2.4", 13 | "stil4m/elm-syntax": "7.3.8" 14 | }, 15 | "indirect": { 16 | "elm/bytes": "1.0.8", 17 | "elm/html": "1.0.0", 18 | "elm/json": "1.1.3", 19 | "elm/parser": "1.1.0", 20 | "elm/project-metadata-utils": "1.0.2", 21 | "elm/random": "1.0.0", 22 | "elm/regex": "1.0.0", 23 | "elm/time": "1.0.0", 24 | "elm/virtual-dom": "1.0.3", 25 | "elm-explorations/test": "2.2.0", 26 | "pzp1997/assoc-list": "1.0.0", 27 | "rtfeldman/elm-hex": "1.0.0", 28 | "stil4m/structured-writer": "1.0.3" 29 | } 30 | }, 31 | "test-dependencies": { 32 | "direct": { 33 | "elm-explorations/test": "2.2.0" 34 | }, 35 | "indirect": {} 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/check-css-vars.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Combine all CSS files and check if CSS variables that are referenced 4 | // actually are defined somewhere in all the files. 5 | // Currently does not verify that the variables referenced are in scope. 6 | // 7 | // TODO: Don't warn for var usages that have fallbacks 8 | import { glob } from "glob"; 9 | import fs from "fs/promises"; 10 | 11 | console.log("👀 Checking CSS variables..."); 12 | 13 | const files = await glob("src/**/*.css"); 14 | 15 | Promise.all(files.map((f) => fs.readFile(f, "utf-8"))) 16 | .then((css) => css.reduce((acc, s) => acc.concat(s))) 17 | .then((allCss) => { 18 | const varsReferencedRegEx = /var\(--.*?\)/g; 19 | const allVarsReferenced = allCss 20 | .match(varsReferencedRegEx) 21 | .filter((v, i, l) => l.indexOf(v) === i) 22 | .reduce((acc, s) => { 23 | if (s.includes(",")) { 24 | const vars = s 25 | .replace(/var\(/g, "") 26 | .replace(/\)/g, "") 27 | .split(",") 28 | .map((s) => s.trim()) 29 | .filter((s) => s.startsWith("--")); 30 | return acc.concat(...vars); 31 | } else { 32 | return acc.concat(s.replace(/var\(/g, "").replace(/\)/g, "").trim()); 33 | } 34 | }, []); 35 | 36 | const varsDefinedRegex = /--.*?\:/g; 37 | const allVarsDefined = allCss 38 | .match(varsDefinedRegex) 39 | .map((s) => s.trim().replace(/:/g, "")) 40 | .filter((v, i, l) => l.indexOf(v) === i); 41 | 42 | const missingVars = allVarsReferenced.reduce((acc, v) => { 43 | if (allVarsDefined.includes(v)) { 44 | return acc; 45 | } else { 46 | return acc.concat(v); 47 | } 48 | }, []); 49 | 50 | if (missingVars.length > 0) { 51 | console.log( 52 | `🚨 Error! ${missingVars.length} undefined CSS variables used:`, 53 | ); 54 | console.log(""); 55 | console.log(missingVars.join("\n")); 56 | process.exit(1); 57 | } else { 58 | console.log("✅ Yay! All CSS variables are accounted for"); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /src/Code/Branch.elm: -------------------------------------------------------------------------------- 1 | module Code.Branch exposing (..) 2 | 3 | import Code.BranchRef as BranchRef exposing (BranchRef) 4 | import Code.Hash as Hash exposing (Hash) 5 | import Json.Decode as Decode 6 | import Json.Decode.Pipeline exposing (required) 7 | import UI.DateTime as DateTime exposing (DateTime) 8 | 9 | 10 | type alias Branch b = 11 | { b 12 | | ref : BranchRef 13 | , createdAt : DateTime 14 | , updatedAt : DateTime 15 | , causalHash : Hash 16 | } 17 | 18 | 19 | type alias BranchSummary p = 20 | Branch { project : p } 21 | 22 | 23 | decodeSummary : Decode.Decoder p -> Decode.Decoder (BranchSummary p) 24 | decodeSummary projectDecoder = 25 | let 26 | makeBranch branchRef project createdAt updatedAt causalHash = 27 | { ref = branchRef 28 | , project = project 29 | , createdAt = createdAt 30 | , updatedAt = updatedAt 31 | , causalHash = causalHash 32 | } 33 | in 34 | Decode.succeed makeBranch 35 | |> required "branchRef" BranchRef.decode 36 | |> required "project" projectDecoder 37 | |> required "createdAt" DateTime.decode 38 | |> required "updatedAt" DateTime.decode 39 | |> required "causalHash" Hash.decode 40 | -------------------------------------------------------------------------------- /src/Code/Browser.elm: -------------------------------------------------------------------------------- 1 | module Code.Browser exposing (..) 2 | 3 | import Code.CodebaseApi as CodebaseApi 4 | import Code.CodebaseTree.NamespaceListing as NamespaceListing exposing (NamespaceListing) 5 | import Code.Config exposing (Config) 6 | import Code.FullyQualifiedName exposing (FQN) 7 | import Code.Namespace.NamespaceRef as NamespaceRef 8 | import Http 9 | import Lib.HttpApi as HttpApi exposing (ApiRequest) 10 | import RemoteData exposing (RemoteData(..)) 11 | import UI.MillerColumns as MillerColumns exposing (MillerColumns) 12 | 13 | 14 | type alias Model = 15 | { columns : MillerColumns NamespaceListing Msg 16 | } 17 | 18 | 19 | init : Config -> ( Model, Cmd Msg ) 20 | init config = 21 | let 22 | columns = 23 | MillerColumns.millerColumns Select Loading 24 | 25 | model = 26 | { columns = columns } 27 | in 28 | ( model 29 | , HttpApi.perform config.api (fetchRootNamespaceListing config) 30 | ) 31 | 32 | 33 | type Msg 34 | = Select NamespaceListing 35 | | FetchSubNamespaceListingFinished FQN (Result Http.Error NamespaceListing) 36 | | FetchRootNamespaceListingFinished (Result Http.Error NamespaceListing) 37 | 38 | 39 | fetchRootNamespaceListing : Config -> ApiRequest NamespaceListing Msg 40 | fetchRootNamespaceListing config = 41 | fetchNamespaceListing config Nothing FetchRootNamespaceListingFinished 42 | 43 | 44 | fetchSubNamespaceListing : Config -> FQN -> ApiRequest NamespaceListing Msg 45 | fetchSubNamespaceListing config fqn = 46 | fetchNamespaceListing config (Just fqn) (FetchSubNamespaceListingFinished fqn) 47 | 48 | 49 | fetchNamespaceListing : Config -> Maybe FQN -> (Result Http.Error NamespaceListing -> msg) -> ApiRequest NamespaceListing msg 50 | fetchNamespaceListing config fqn toMsg = 51 | CodebaseApi.Browse { perspective = config.perspective, ref = Maybe.map NamespaceRef.NameRef fqn } 52 | |> config.toApiEndpoint 53 | |> HttpApi.toRequest (NamespaceListing.decode fqn) toMsg 54 | -------------------------------------------------------------------------------- /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 | | Summary { perspective : Perspective, ref : Reference } 36 | 37 | 38 | type alias ToApiEndpoint = 39 | CodebaseEndpoint -> HttpApi.Endpoint 40 | -------------------------------------------------------------------------------- /src/Code/Config.elm: -------------------------------------------------------------------------------- 1 | module Code.Config exposing (..) 2 | 3 | import Code.CodebaseApi exposing (ToApiEndpoint) 4 | import Code.Perspective exposing (Perspective) 5 | import Lib.HttpApi exposing (HttpApi) 6 | import Lib.OperatingSystem exposing (OperatingSystem) 7 | 8 | 9 | type alias Config = 10 | { operatingSystem : OperatingSystem 11 | , perspective : Perspective 12 | , toApiEndpoint : ToApiEndpoint 13 | , api : HttpApi 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 | , rawSource 10 | ) 11 | 12 | import Code.Definition.Info exposing (Info) 13 | import Code.Definition.Term as Term exposing (TermSignature) 14 | import Code.Definition.Type as Type exposing (TypeSource) 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 19 | 20 | 21 | type AbilityConstructorSource 22 | = Source Syntax 23 | | Builtin 24 | 25 | 26 | type AbilityConstructor a 27 | = AbilityConstructor Hash a 28 | 29 | 30 | type alias AbilityConstructorDetail = 31 | AbilityConstructor { info : Info, source : TypeSource, signature : TermSignature } 32 | 33 | 34 | type alias AbilityConstructorSummary = 35 | AbilityConstructor 36 | { fqn : FQN 37 | , name : FQN 38 | , namespace : Maybe String 39 | , signature : TermSignature 40 | } 41 | 42 | 43 | type alias AbilityConstructorListing = 44 | AbilityConstructor FQN 45 | 46 | 47 | 48 | -- HELPERS 49 | 50 | 51 | rawSource : AbilityConstructorDetail -> Maybe String 52 | rawSource (AbilityConstructor _ { source }) = 53 | case source of 54 | Type.Source stx -> 55 | Just (Syntax.toString stx) 56 | 57 | Type.Builtin -> 58 | Nothing 59 | 60 | 61 | 62 | -- JSON DECODERS 63 | 64 | 65 | decodeSource : List String -> List String -> Decode.Decoder TypeSource 66 | decodeSource = 67 | Type.decodeTypeSource 68 | 69 | 70 | decodeSignature : List String -> Decode.Decoder TermSignature 71 | decodeSignature = 72 | Term.decodeSignature 73 | -------------------------------------------------------------------------------- /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 | , rawSource 10 | ) 11 | 12 | import Code.Definition.Info exposing (Info) 13 | import Code.Definition.Term as Term exposing (TermSignature) 14 | import Code.Definition.Type as Type exposing (TypeSource) 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 19 | 20 | 21 | type DataConstructorSource 22 | = Source Syntax 23 | | Builtin 24 | 25 | 26 | type DataConstructor a 27 | = DataConstructor Hash a 28 | 29 | 30 | type alias DataConstructorDetail = 31 | DataConstructor { info : Info, source : TypeSource, signature : TermSignature } 32 | 33 | 34 | type alias DataConstructorSummary = 35 | DataConstructor 36 | { fqn : FQN 37 | , name : FQN 38 | , namespace : Maybe String 39 | , signature : TermSignature 40 | } 41 | 42 | 43 | type alias DataConstructorListing = 44 | DataConstructor FQN 45 | 46 | 47 | 48 | -- HELPERS 49 | 50 | 51 | rawSource : DataConstructorDetail -> Maybe String 52 | rawSource (DataConstructor _ { source }) = 53 | case source of 54 | Type.Source stx -> 55 | Just (Syntax.toString stx) 56 | 57 | Type.Builtin -> 58 | Nothing 59 | 60 | 61 | 62 | -- JSON DECODERS 63 | 64 | 65 | decodeSource : List String -> List String -> Decode.Decoder TypeSource 66 | decodeSource = 67 | Type.decodeTypeSource 68 | 69 | 70 | decodeSignature : List String -> Decode.Decoder TermSignature 71 | decodeSignature = 72 | Term.decodeSignature 73 | -------------------------------------------------------------------------------- /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 ref suffixName allFqns = 26 | let 27 | ( namespace, otherNames ) = 28 | namespaceAndOtherNames ref 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 | |> ListE.uniqueBy FQN.toString 69 | in 70 | ( FQN.namespace fqnWithin, fqnsWithout ) 71 | -------------------------------------------------------------------------------- /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.Syntax.SyntaxConfig exposing (SyntaxConfig) 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 | SyntaxConfig msg 24 | -> (FoldId -> msg) 25 | -> DocFoldToggles 26 | -> Readme 27 | -> Html msg 28 | view syntaxCfg toggleFoldMsg docFoldToggles (Readme doc) = 29 | div [ class "readme" ] 30 | [ Doc.view syntaxCfg 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/EmptyState.elm: -------------------------------------------------------------------------------- 1 | module Code.EmptyState exposing (..) 2 | 3 | import Html exposing (Html, div, h2, header, p, text) 4 | import Html.Attributes exposing (class) 5 | import UI.Button as Button 6 | import UI.Click exposing (Click) 7 | import UI.Icon as Icon 8 | import UI.Placeholder as Placeholder 9 | 10 | 11 | viewFauxDefinition : Html msg 12 | viewFauxDefinition = 13 | div [ class "code_empty-state_faux-definition" ] 14 | [ Placeholder.text 15 | |> Placeholder.withSize Placeholder.Large 16 | |> Placeholder.withLength Placeholder.Medium 17 | |> Placeholder.view 18 | , Placeholder.text 19 | |> Placeholder.withSize Placeholder.Large 20 | |> Placeholder.withLength Placeholder.Large 21 | |> Placeholder.withIntensity Placeholder.Subdued 22 | |> Placeholder.view 23 | ] 24 | 25 | 26 | view : String -> Click msg -> Html msg 27 | view title click = 28 | div [ class "code_empty-state" ] 29 | [ div [ class "code_empty-state_content" ] 30 | [ header [] 31 | [ h2 [] [ text title ] 32 | , p [] [ text "Browse, search, read docs, open definitions, and explore." ] 33 | ] 34 | , viewFauxDefinition 35 | , viewFauxDefinition 36 | , Button.iconThenLabel_ click Icon.search "Search" 37 | |> Button.emphasized 38 | |> Button.view 39 | ] 40 | ] 41 | -------------------------------------------------------------------------------- /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 adhocFqn = 25 | case ( perspective, adhocFqn ) 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/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/ProjectDependency.elm: -------------------------------------------------------------------------------- 1 | module Code.ProjectDependency exposing (ProjectDependency, fromString, toString, toTag) 2 | 3 | import Code.Version as Version exposing (Version) 4 | import Maybe.Extra as MaybeE 5 | import UI.Tag as Tag exposing (Tag) 6 | 7 | 8 | type alias ProjectDependency = 9 | { name : String, version : Maybe Version } 10 | 11 | 12 | fromString : String -> ProjectDependency 13 | fromString raw = 14 | let 15 | parts = 16 | String.split "_" raw 17 | 18 | ( name, version ) = 19 | case parts of 20 | [ user, project, major, minor, patch ] -> 21 | ( "@" ++ user ++ "/" ++ project, Version.fromString (String.join "." [ major, minor, patch ]) ) 22 | 23 | [ n, major, minor, patch ] -> 24 | let 25 | version_ = 26 | Version.fromString (String.join "." [ major, minor, patch ]) 27 | in 28 | case version_ of 29 | Just v -> 30 | ( n, Just v ) 31 | 32 | -- It wasn't a version after all, so we give up trying to parse it 33 | Nothing -> 34 | ( raw, Nothing ) 35 | 36 | [ user, project ] -> 37 | ( "@" ++ user ++ "/" ++ project, Nothing ) 38 | 39 | [ n ] -> 40 | ( n, Nothing ) 41 | 42 | _ -> 43 | case List.reverse parts of 44 | patch :: minor :: major :: n -> 45 | let 46 | version_ = 47 | Version.fromString (String.join "." [ major, minor, patch ]) 48 | in 49 | case version_ of 50 | Just v -> 51 | ( String.join "_" (List.reverse n), Just v ) 52 | 53 | -- It wasn't a version after all, so we give up trying to parse it 54 | Nothing -> 55 | ( raw, Nothing ) 56 | 57 | _ -> 58 | ( raw, Nothing ) 59 | in 60 | ProjectDependency name version 61 | 62 | 63 | toString : ProjectDependency -> String 64 | toString { name, version } = 65 | name ++ MaybeE.unwrap "" (\v -> " v" ++ Version.toString v) version 66 | 67 | 68 | toTag : ProjectDependency -> Tag msg 69 | toTag { name, version } = 70 | name 71 | |> Tag.tag 72 | |> Tag.large 73 | |> Tag.withRightText (MaybeE.unwrap "" (\v -> " v" ++ Version.toString v) version) 74 | -------------------------------------------------------------------------------- /src/Code/ProjectSlug.elm: -------------------------------------------------------------------------------- 1 | module Code.ProjectSlug exposing 2 | ( ProjectSlug 3 | , decode 4 | , equals 5 | , fromString 6 | , isValidProjectSlug 7 | , toNamespaceString 8 | , toString 9 | , unsafeFromString 10 | ) 11 | 12 | import Json.Decode as Decode exposing (string) 13 | import Regex 14 | import String.Extra exposing (camelize) 15 | 16 | 17 | type ProjectSlug 18 | = ProjectSlug String 19 | 20 | 21 | fromString : String -> Maybe ProjectSlug 22 | fromString raw = 23 | let 24 | validate s = 25 | if isValidProjectSlug s then 26 | Just s 27 | 28 | else 29 | Nothing 30 | in 31 | raw 32 | |> validate 33 | |> Maybe.map ProjectSlug 34 | 35 | 36 | {-| Don't use, meant for ease of testing 37 | -} 38 | unsafeFromString : String -> ProjectSlug 39 | unsafeFromString raw = 40 | ProjectSlug raw 41 | 42 | 43 | toString : ProjectSlug -> String 44 | toString (ProjectSlug raw) = 45 | raw 46 | 47 | 48 | {-| namespaces and project slugs don't completely overlap in their validity. 49 | convert slugs into namespaces, making sure to camelize any underscore or dash 50 | separated words 51 | -} 52 | toNamespaceString : ProjectSlug -> String 53 | toNamespaceString = 54 | toString >> camelize 55 | 56 | 57 | equals : ProjectSlug -> ProjectSlug -> Bool 58 | equals (ProjectSlug a) (ProjectSlug b) = 59 | a == b 60 | 61 | 62 | {-| Requirements 63 | 64 | - May only contain alphanumeric characters, underscores, and hyphens. 65 | - No special symbols or spaces 66 | - no slashes 67 | - Can't be a reserved word, like "code" or "p" (those are used in URLs to mean other things) 68 | 69 | -} 70 | isValidProjectSlug : String -> Bool 71 | isValidProjectSlug raw = 72 | let 73 | reserved = 74 | [ "code", "p" ] 75 | 76 | isReserved = 77 | List.member (String.toLower raw) reserved 78 | 79 | re = 80 | Maybe.withDefault Regex.never <| 81 | Regex.fromString "^[\\w-]+$" 82 | in 83 | not isReserved && Regex.contains re raw 84 | 85 | 86 | decode : Decode.Decoder ProjectSlug 87 | decode = 88 | let 89 | decode_ s = 90 | case fromString s of 91 | Just u -> 92 | Decode.succeed u 93 | 94 | Nothing -> 95 | Decode.fail "Could not parse as ProjectSlug" 96 | in 97 | Decode.andThen decode_ string 98 | -------------------------------------------------------------------------------- /src/Code/README.md: -------------------------------------------------------------------------------- 1 | # Code 2 | 3 | A Unison UI library for rendering Unison Code. 4 | -------------------------------------------------------------------------------- /src/Code/Source/SourceViewConfig.elm: -------------------------------------------------------------------------------- 1 | module Code.Source.SourceViewConfig exposing 2 | ( SourceViewConfig 3 | , monochrome 4 | , plain 5 | , rich 6 | , rich_ 7 | , toClassName 8 | , toSyntaxConfig 9 | ) 10 | 11 | import Code.Syntax.SyntaxConfig as SyntaxConfig exposing (SyntaxConfig) 12 | 13 | 14 | type SourceViewConfig msg 15 | = Rich (SyntaxConfig msg) 16 | | Monochrome 17 | | Plain 18 | 19 | 20 | rich : SyntaxConfig msg -> SourceViewConfig msg 21 | rich syntaxConfig = 22 | rich_ syntaxConfig 23 | 24 | 25 | rich_ : SyntaxConfig msg -> SourceViewConfig msg 26 | rich_ = 27 | Rich 28 | 29 | 30 | monochrome : SourceViewConfig msg 31 | monochrome = 32 | Monochrome 33 | 34 | 35 | plain : SourceViewConfig msg 36 | plain = 37 | Plain 38 | 39 | 40 | 41 | -- HELPERS 42 | 43 | 44 | toClassName : SourceViewConfig msg -> String 45 | toClassName viewConfig = 46 | case viewConfig of 47 | Rich _ -> 48 | "rich" 49 | 50 | Monochrome -> 51 | "monochrome" 52 | 53 | Plain -> 54 | "plain" 55 | 56 | 57 | toSyntaxConfig : SourceViewConfig msg -> SyntaxConfig msg 58 | toSyntaxConfig viewConfig = 59 | case viewConfig of 60 | Rich syntaxConfig -> 61 | syntaxConfig 62 | 63 | _ -> 64 | SyntaxConfig.empty 65 | -------------------------------------------------------------------------------- /src/Code/Syntax/SyntaxConfig.elm: -------------------------------------------------------------------------------- 1 | module Code.Syntax.SyntaxConfig exposing (..) 2 | 3 | import Code.Definition.Reference exposing (Reference) 4 | import UI.Click exposing (Click) 5 | import UI.Tooltip exposing (Tooltip) 6 | 7 | 8 | type alias TooltipConfig msg = 9 | { toHoverStart : Reference -> msg 10 | , toHoverEnd : Reference -> msg 11 | , toTooltip : Reference -> Maybe (Tooltip msg) 12 | } 13 | 14 | 15 | type alias ToClick msg = 16 | Reference -> Click msg 17 | 18 | 19 | type alias SyntaxConfig msg = 20 | { toClick : Maybe (ToClick msg) 21 | , dependencyTooltip : Maybe (TooltipConfig msg) 22 | , showSyntaxHelpTooltip : Bool 23 | } 24 | 25 | 26 | 27 | -- CREATE 28 | 29 | 30 | empty : SyntaxConfig msg 31 | empty = 32 | { toClick = Nothing, dependencyTooltip = Nothing, showSyntaxHelpTooltip = False } 33 | 34 | 35 | default : ToClick msg -> TooltipConfig msg -> SyntaxConfig msg 36 | default toClick tooltipConfig = 37 | empty 38 | |> withToClick toClick 39 | |> withDependencyTooltip tooltipConfig 40 | |> withSyntaxHelp 41 | 42 | 43 | 44 | -- MODIFY 45 | 46 | 47 | withToClick : ToClick msg -> SyntaxConfig msg -> SyntaxConfig msg 48 | withToClick toClick cfg = 49 | { cfg | toClick = Just toClick } 50 | 51 | 52 | withDependencyTooltip : TooltipConfig msg -> SyntaxConfig msg -> SyntaxConfig msg 53 | withDependencyTooltip tooltipConfig cfg = 54 | { cfg | dependencyTooltip = Just tooltipConfig } 55 | 56 | 57 | withSyntaxHelp : SyntaxConfig msg -> SyntaxConfig msg 58 | withSyntaxHelp cfg = 59 | { cfg | showSyntaxHelpTooltip = True } 60 | 61 | 62 | withoutSyntaxHelp : SyntaxConfig msg -> SyntaxConfig msg 63 | withoutSyntaxHelp cfg = 64 | { cfg | showSyntaxHelpTooltip = False } 65 | -------------------------------------------------------------------------------- /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/Aria.elm: -------------------------------------------------------------------------------- 1 | module Lib.Aria exposing (..) 2 | 3 | import Html exposing (Attribute) 4 | import Html.Attributes exposing (attribute) 5 | 6 | 7 | role : String -> Attribute msg 8 | role r = 9 | attribute "role" r 10 | 11 | 12 | ariaLabel : String -> Attribute msg 13 | ariaLabel l = 14 | attribute "aria-label" l 15 | -------------------------------------------------------------------------------- /src/Lib/Decode/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Lib.Decode.Helpers exposing (..) 2 | 3 | import Json.Decode as Decode 4 | import Json.Decode.Extra exposing (when) 5 | import Json.Decode.Pipeline exposing (optionalAt) 6 | import List.Nonempty as NEL 7 | import Url exposing (Url) 8 | 9 | 10 | maybeAt : List String -> Decode.Decoder b -> Decode.Decoder (Maybe b -> c) -> Decode.Decoder c 11 | maybeAt path decode = 12 | optionalAt path (Decode.map Just decode) Nothing 13 | 14 | 15 | nonEmptyList : Decode.Decoder a -> Decode.Decoder (NEL.Nonempty a) 16 | nonEmptyList = 17 | Decode.list 18 | >> Decode.andThen 19 | (\list -> 20 | case NEL.fromList list of 21 | Just nel -> 22 | Decode.succeed nel 23 | 24 | Nothing -> 25 | Decode.fail "Decoded an empty list" 26 | ) 27 | 28 | 29 | failInvalid : String -> Maybe a -> Decode.Decoder a 30 | failInvalid failMessage m = 31 | case m of 32 | Nothing -> 33 | Decode.fail failMessage 34 | 35 | Just a -> 36 | Decode.succeed a 37 | 38 | 39 | tag : Decode.Decoder String 40 | tag = 41 | Decode.field "tag" Decode.string 42 | 43 | 44 | kind : Decode.Decoder String 45 | kind = 46 | Decode.field "tag" Decode.string 47 | 48 | 49 | whenTagIs : String -> Decode.Decoder a -> Decode.Decoder a 50 | whenTagIs val = 51 | whenPathIs [ "tag" ] val 52 | 53 | 54 | whenKindIs : String -> Decode.Decoder a -> Decode.Decoder a 55 | whenKindIs val = 56 | whenPathIs [ "kind" ] val 57 | 58 | 59 | whenPathIs : List String -> String -> Decode.Decoder a -> Decode.Decoder a 60 | whenPathIs path val = 61 | when (Decode.at path Decode.string) ((==) val) 62 | 63 | 64 | whenFieldIs : String -> String -> Decode.Decoder a -> Decode.Decoder a 65 | whenFieldIs fieldName val = 66 | when (Decode.field fieldName Decode.string) ((==) val) 67 | 68 | 69 | url : Decode.Decoder Url 70 | url = 71 | let 72 | decodeUrl_ s = 73 | case Url.fromString s of 74 | Just u -> 75 | Decode.succeed u 76 | 77 | Nothing -> 78 | Decode.fail "Could not parse as URL" 79 | in 80 | Decode.andThen decodeUrl_ Decode.string 81 | -------------------------------------------------------------------------------- /src/Lib/EmbedKatex.elm: -------------------------------------------------------------------------------- 1 | module Lib.EmbedKatex exposing (..) 2 | 3 | import Html exposing (Html, node) 4 | import Html.Attributes exposing (attribute, id) 5 | 6 | 7 | type KatexDisplay 8 | = Inline 9 | | Block 10 | 11 | 12 | type alias Katex = 13 | { markup : String 14 | , display : KatexDisplay 15 | } 16 | 17 | 18 | katex : String -> Katex 19 | katex markup = 20 | { markup = markup, display = Block } 21 | 22 | 23 | withDisplay : KatexDisplay -> Katex -> Katex 24 | withDisplay display k = 25 | { k | display = display } 26 | 27 | 28 | asInline : Katex -> Katex 29 | asInline k = 30 | { k | display = Inline } 31 | 32 | 33 | asBlock : Katex -> Katex 34 | asBlock k = 35 | { k | display = Block } 36 | 37 | 38 | view : Katex -> Html msg 39 | view k = 40 | let 41 | displayToString d = 42 | case d of 43 | Inline -> 44 | "inline" 45 | 46 | Block -> 47 | "block" 48 | in 49 | node "embed-katex" 50 | [ id "embed-katex" 51 | , attribute "markup" k.markup 52 | , attribute "display" (displayToString k.display) 53 | ] 54 | [] 55 | -------------------------------------------------------------------------------- /src/Lib/EmbedKatex.js: -------------------------------------------------------------------------------- 1 | import katex from "katex"; 2 | import "katex/dist/katex.min.css"; 3 | 4 | // 5 | // 6 | 7 | class EmbedKatex extends HTMLElement { 8 | constructor() { 9 | super(); 10 | } 11 | 12 | connectedCallback() { 13 | const markup = this.getAttribute("markup"); 14 | const display = this.getAttribute("display"); 15 | 16 | katex.render(markup, this, { 17 | throwOnError: false, 18 | displayMode: display === "block", 19 | }); 20 | } 21 | } 22 | 23 | customElements.define("embed-katex", EmbedKatex); 24 | -------------------------------------------------------------------------------- /src/Lib/EmbedSvg.elm: -------------------------------------------------------------------------------- 1 | module Lib.EmbedSvg exposing (..) 2 | 3 | import Html exposing (Html, node, text) 4 | import Html.Attributes exposing (id) 5 | 6 | 7 | type alias Svg = 8 | { markup : String 9 | } 10 | 11 | 12 | svg : String -> Svg 13 | svg markup = 14 | { markup = markup } 15 | 16 | 17 | view : Svg -> Html msg 18 | view { markup } = 19 | node "embed-svg" [ id "embed-svg" ] [ text markup ] 20 | -------------------------------------------------------------------------------- /src/Lib/MermaidDiagram.elm: -------------------------------------------------------------------------------- 1 | module Lib.MermaidDiagram exposing (..) 2 | 3 | import Html exposing (Html, node) 4 | import Html.Attributes exposing (attribute) 5 | 6 | 7 | type alias ThemeConfig = 8 | {} 9 | 10 | 11 | type Theme 12 | = Predefined String 13 | | Custom ThemeConfig 14 | 15 | 16 | type alias Mermaid = 17 | { diagram : String, theme : Theme } 18 | 19 | 20 | 21 | -- CREATE 22 | 23 | 24 | mermaid : String -> Mermaid 25 | mermaid diagram = 26 | { diagram = diagram, theme = Predefined "neutral" } 27 | 28 | 29 | 30 | -- MODIFY 31 | 32 | 33 | withTheme : String -> Mermaid -> Mermaid 34 | withTheme themeName m = 35 | withTheme_ (Predefined themeName) m 36 | 37 | 38 | withCustomTheme : ThemeConfig -> Mermaid -> Mermaid 39 | withCustomTheme themeCfg m = 40 | withTheme_ (Custom themeCfg) m 41 | 42 | 43 | withTheme_ : Theme -> Mermaid -> Mermaid 44 | withTheme_ theme m = 45 | { m | theme = theme } 46 | 47 | 48 | 49 | -- VIEW 50 | 51 | 52 | view : Mermaid -> Html msg 53 | view { diagram, theme } = 54 | let 55 | themeAttr = 56 | case theme of 57 | Predefined themeName -> 58 | attribute "theme-name" themeName 59 | 60 | Custom _ -> 61 | attribute "custom-theme" "custom" 62 | in 63 | node "mermaid-diagram" [ attribute "diagram" diagram, themeAttr ] [] 64 | -------------------------------------------------------------------------------- /src/Lib/MermaidDiagram.js: -------------------------------------------------------------------------------- 1 | import mermaid from "mermaid"; 2 | 3 | // 4 | // 5 | 6 | class MermaidDiagram extends HTMLElement { 7 | constructor() { 8 | super(); 9 | } 10 | 11 | async connectedCallback() { 12 | const diagram = this.getAttribute("diagram"); 13 | const themeName = this.getAttribute("theme-name"); 14 | 15 | // Generate a unique-ish diagram id, so we can have more than 1 diagram on 16 | // the page at a time 17 | const diagramId = "mermaid-diagram_" + Date.now().toString(); 18 | 19 | try { 20 | mermaid.initialize({ 21 | theme: themeName, 22 | startOnLoad: false, 23 | securityLevel: "sandbox", 24 | }); 25 | 26 | const { svg } = await mermaid.render(diagramId, diagram); 27 | 28 | this.innerHTML = svg; 29 | const iframe = this.querySelector("iframe"); 30 | iframe?.classList?.add("mermaid-diagram"); 31 | } catch (e) { 32 | const err = document.createElement("div"); 33 | 34 | err.textContent = 35 | "🆘 Unfortunately, the Mermaid diagram could not be rendered."; 36 | err.classList.add("mermaid-diagram"); 37 | err.classList.add("mermaid-diagram_error"); 38 | err.setAttribute("title", e.toString()); 39 | 40 | this.appendChild(err); 41 | 42 | // When Mermaid fails, it sometimes leaves an orphaned iframe at the end 43 | // of . Remove it. 44 | document.getElementById("i" + diagramId)?.remove(); 45 | 46 | // Rethrow error so we can ensure we log it. 47 | throw e; 48 | } 49 | } 50 | } 51 | 52 | customElements.define("mermaid-diagram", MermaidDiagram); 53 | -------------------------------------------------------------------------------- /src/Lib/OnClickOutside.elm: -------------------------------------------------------------------------------- 1 | module Lib.OnClickOutside exposing (onClickOutside) 2 | 3 | import Html exposing (Html, node) 4 | import Html.Attributes exposing (id) 5 | import Html.Events exposing (on) 6 | import Json.Decode as Decode 7 | 8 | 9 | onClickOutside : msg -> Html msg -> Html msg 10 | onClickOutside clickOutsideMsg content = 11 | node "on-click-outside" 12 | [ id "on-click-outside", on "clickOutside" (Decode.succeed clickOutsideMsg) ] 13 | [ content ] 14 | -------------------------------------------------------------------------------- /src/Lib/OnClickOutside.js: -------------------------------------------------------------------------------- 1 | // 2 | //
3 | // 4 | // 5 | class OnClickOutside extends HTMLElement { 6 | constructor() { 7 | super(); 8 | } 9 | 10 | connectedCallback() { 11 | this.onMouseDown = (e) => { 12 | // If the element isn't visible, there's no point in triggering the 13 | // clickoutside event. This is useful when multiple variants of a 14 | // clickoutside is active for the same UI (one for desktop and one for 15 | // mobile for instance). 16 | const style = window.getComputedStyle(this); 17 | 18 | if (style.display === "none" || this.offsetParent === null) { 19 | return; 20 | } 21 | 22 | const isOutside = !this.contains(e.target); 23 | 24 | if (isOutside) { 25 | const event = new CustomEvent("clickOutside"); 26 | this.dispatchEvent(event); 27 | } 28 | }; 29 | 30 | window.addEventListener("mousedown", this.onMouseDown); 31 | } 32 | 33 | disconnectedCallback() { 34 | window.removeEventListener("mousedown", this.onMouseDown); 35 | } 36 | } 37 | 38 | customElements.define("on-click-outside", OnClickOutside); 39 | -------------------------------------------------------------------------------- /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/Paginated.elm: -------------------------------------------------------------------------------- 1 | module Lib.Paginated exposing (..) 2 | 3 | 4 | type Paginated a 5 | = Paginated 6 | { cursor : String 7 | , perPage : Int 8 | , total : Int 9 | , items : List a 10 | } 11 | -------------------------------------------------------------------------------- /src/Lib/README.md: -------------------------------------------------------------------------------- 1 | # Lib 2 | 3 | Common utility types and helpers. Does not include any UI directly. 4 | -------------------------------------------------------------------------------- /src/Lib/ScrollEvent.elm: -------------------------------------------------------------------------------- 1 | module Lib.ScrollEvent exposing (..) 2 | 3 | import Json.Decode as Decode exposing (int) 4 | import Json.Decode.Pipeline exposing (requiredAt) 5 | 6 | 7 | type alias ScrollEvent = 8 | { scrollHeight : Int 9 | , scrollLeft : Int 10 | , scrollTop : Int 11 | , scrollWidth : Int 12 | , clientHeight : Int 13 | , clientLeft : Int 14 | , clientTop : Int 15 | , clientWidth : Int 16 | } 17 | 18 | 19 | decode : Decode.Decoder ScrollEvent 20 | decode = 21 | Decode.succeed ScrollEvent 22 | |> requiredAt [ "currentTarget", "scrollHeight" ] int 23 | |> requiredAt [ "currentTarget", "scrollLeft" ] int 24 | |> requiredAt [ "currentTarget", "scrollTop" ] int 25 | |> requiredAt [ "currentTarget", "scrollWidth" ] int 26 | |> requiredAt [ "currentTarget", "clientHeight" ] int 27 | |> requiredAt [ "currentTarget", "clientLeft" ] int 28 | |> requiredAt [ "currentTarget", "clientTop" ] int 29 | |> requiredAt [ "currentTarget", "clientWidth" ] int 30 | 31 | 32 | decodeToMsg : (ScrollEvent -> msg) -> Decode.Decoder msg 33 | decodeToMsg toMsg = 34 | Decode.map toMsg decode 35 | -------------------------------------------------------------------------------- /src/Lib/ScrollTo.elm: -------------------------------------------------------------------------------- 1 | module Lib.ScrollTo exposing (..) 2 | 3 | import Browser.Dom as Dom 4 | import Task 5 | 6 | 7 | scrollTo : msg -> String -> String -> Cmd msg 8 | scrollTo doneMsg containerId targetId = 9 | scrollTo_ doneMsg containerId targetId 0 10 | 11 | 12 | scrollTo_ : msg -> String -> String -> Float -> Cmd msg 13 | scrollTo_ doneMsg containerId targetId marginTop = 14 | Task.sequence 15 | [ Dom.getElement targetId |> Task.map (.element >> .y) 16 | , Dom.getElement containerId |> Task.map (.element >> .y) 17 | , Dom.getViewportOf containerId |> Task.map (.viewport >> .y) 18 | ] 19 | |> Task.andThen 20 | (\ys -> 21 | case ys of 22 | elY :: viewportY :: viewportScrollTop :: [] -> 23 | Dom.setViewportOf containerId 0 (viewportScrollTop + (elY - viewportY) - marginTop) 24 | |> Task.onError (\_ -> Task.succeed ()) 25 | 26 | _ -> 27 | Task.succeed () 28 | ) 29 | |> Task.attempt (always doneMsg) 30 | -------------------------------------------------------------------------------- /src/Lib/Slug.elm: -------------------------------------------------------------------------------- 1 | module Lib.Slug exposing 2 | ( Slug 3 | , decode 4 | , equals 5 | , fromString 6 | , isValidSlug 7 | , toString 8 | , unsafeFromString 9 | ) 10 | 11 | import Json.Decode as Decode exposing (string) 12 | import Regex 13 | 14 | 15 | type Slug 16 | = Slug String 17 | 18 | 19 | fromString : String -> Maybe Slug 20 | fromString raw = 21 | let 22 | validate s = 23 | if isValidSlug s then 24 | Just s 25 | 26 | else 27 | Nothing 28 | in 29 | raw 30 | |> validate 31 | |> Maybe.map Slug 32 | 33 | 34 | {-| Don't use, meant for ease of testing 35 | -} 36 | unsafeFromString : String -> Slug 37 | unsafeFromString raw = 38 | Slug raw 39 | 40 | 41 | toString : Slug -> String 42 | toString (Slug raw) = 43 | raw 44 | 45 | 46 | equals : Slug -> Slug -> Bool 47 | equals (Slug a) (Slug b) = 48 | a == b 49 | 50 | 51 | {-| Modelled after the GitHub user handle requirements, since we're importing their handles. 52 | 53 | Validates an un-prefixed string. So `@unison` would not be valid. We add and 54 | remove `@` as a toString/fromString step instead 55 | 56 | Requirements (via ): 57 | 58 | - May only contain alphanumeric characters or hyphens. 59 | - Can't have multiple consecutive hyphens. 60 | - Can't begin or end with a hyphen. 61 | - Maximum is 39 characters. 62 | 63 | -} 64 | isValidSlug : String -> Bool 65 | isValidSlug raw = 66 | let 67 | re = 68 | Maybe.withDefault Regex.never <| 69 | Regex.fromString "^[a-z\\d](?:[a-z\\d]|-(?=[a-z\\d])){1,39}$" 70 | in 71 | Regex.contains re raw 72 | 73 | 74 | decode : Decode.Decoder Slug 75 | decode = 76 | let 77 | decode_ s = 78 | case fromString s of 79 | Just u -> 80 | Decode.succeed u 81 | 82 | Nothing -> 83 | Decode.fail "Could not parse as Slug" 84 | in 85 | Decode.andThen decode_ string 86 | -------------------------------------------------------------------------------- /src/Lib/String/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Lib.String.Helpers exposing (..) 2 | 3 | {-| A unicode aware string length 4 | -} 5 | 6 | 7 | unicodeLength : String -> Int 8 | unicodeLength s = 9 | s |> String.toList |> List.length 10 | 11 | 12 | pluralize : String -> String -> Int -> String 13 | pluralize singular plural num = 14 | if num == 1 then 15 | singular 16 | 17 | else 18 | plural 19 | 20 | 21 | possessive : String -> String 22 | possessive s = 23 | if String.endsWith "s" s then 24 | s ++ "'" 25 | 26 | else 27 | s ++ "'s" 28 | -------------------------------------------------------------------------------- /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 could 7 | have lists as data. Definition.Doc is the main example of a tree structure 8 | that uses TreePath. Definition.Doc uses TreePath for tracking if sub trees of 9 | a Doc is folded or not. 10 | 11 | --} 12 | 13 | 14 | module Lib.TreePath exposing (TreePath, TreePathItem(..), toString) 15 | 16 | 17 | type TreePathItem 18 | = VariantIndex Int 19 | | ListIndex Int 20 | 21 | 22 | type alias TreePath = 23 | List TreePathItem 24 | 25 | 26 | toString : TreePath -> String 27 | toString path = 28 | let 29 | pathItemToString item = 30 | case item of 31 | VariantIndex i -> 32 | "VariantIndex#" ++ String.fromInt i 33 | 34 | ListIndex i -> 35 | "ListIndex#" ++ String.fromInt i 36 | in 37 | path |> List.map pathItemToString |> String.join "." 38 | -------------------------------------------------------------------------------- /src/Lib/UnicodeSort.elm: -------------------------------------------------------------------------------- 1 | module Lib.UnicodeSort exposing (compareUnicode) 2 | 3 | {- 4 | Sort Strings by [RFC5051](http://www.rfc-editor.org/rfc/rfc5051.txt) 5 | 6 | Elm port of https://github.com/jgm/rfc5051 7 | -} 8 | 9 | import Char 10 | import Dict 11 | import Lib.UnicodeSort.UnicodeData exposing (decompositionMap) 12 | import String 13 | 14 | 15 | {-| Compare two strings using `unicode-casemap` 16 | the simple unicode collation algorithm described in RFC 5051. 17 | -} 18 | compareUnicode : String -> String -> Order 19 | compareUnicode x y = 20 | case ( String.uncons x, String.uncons y ) of 21 | ( Nothing, Nothing ) -> 22 | EQ 23 | 24 | ( Nothing, Just _ ) -> 25 | LT 26 | 27 | ( Just _, Nothing ) -> 28 | GT 29 | 30 | ( Just ( xc, x_ ), Just ( yc, y_ ) ) -> 31 | case compare (canonicalize xc) (canonicalize yc) of 32 | GT -> 33 | GT 34 | 35 | LT -> 36 | LT 37 | 38 | EQ -> 39 | compareUnicode x_ y_ 40 | 41 | 42 | canonicalize : Char -> List Int 43 | canonicalize = 44 | decompose << Char.toCode << Char.toUpper 45 | 46 | 47 | decompose : Int -> List Int 48 | decompose c = 49 | case decompose_ c of 50 | Nothing -> 51 | [ c ] 52 | 53 | Just xs -> 54 | List.concatMap decompose xs 55 | 56 | 57 | decompose_ : Int -> Maybe (List Int) 58 | decompose_ c = 59 | Dict.get c decompositionMap 60 | -------------------------------------------------------------------------------- /src/Lib/UserHandle.elm: -------------------------------------------------------------------------------- 1 | module Lib.UserHandle exposing 2 | ( UserHandle 3 | , decode 4 | , decodeUnprefixed 5 | , equals 6 | , fromSlug 7 | , fromString 8 | , fromUnprefixedString 9 | , isValidHandle 10 | , toString 11 | , toUnprefixedString 12 | , unsafeFromString 13 | ) 14 | 15 | import Json.Decode as Decode exposing (string) 16 | import Lib.Slug as Slug exposing (Slug) 17 | 18 | 19 | type UserHandle 20 | = UserHandle Slug 21 | 22 | 23 | fromSlug : Slug -> UserHandle 24 | fromSlug slug = 25 | UserHandle slug 26 | 27 | 28 | fromString : String -> Maybe UserHandle 29 | fromString raw = 30 | if String.startsWith "@" raw then 31 | raw 32 | |> String.dropLeft 1 33 | |> Slug.fromString 34 | |> Maybe.map UserHandle 35 | 36 | else 37 | Nothing 38 | 39 | 40 | {-| Don't use, meant for ease of testing 41 | -} 42 | unsafeFromString : String -> UserHandle 43 | unsafeFromString raw = 44 | raw |> Slug.unsafeFromString |> UserHandle 45 | 46 | 47 | fromUnprefixedString : String -> Maybe UserHandle 48 | fromUnprefixedString raw = 49 | if String.startsWith "@" raw then 50 | Nothing 51 | 52 | else 53 | raw |> Slug.fromString |> Maybe.map UserHandle 54 | 55 | 56 | {-| Validates an un-prefixed string. So `@unison` would not be valid. We add and 57 | remove `@` as a toString/fromString step instead 58 | -} 59 | isValidHandle : String -> Bool 60 | isValidHandle unprefixed = 61 | if String.contains "@" unprefixed then 62 | False 63 | 64 | else 65 | Slug.isValidSlug unprefixed 66 | 67 | 68 | toString : UserHandle -> String 69 | toString (UserHandle slug) = 70 | "@" ++ Slug.toString slug 71 | 72 | 73 | toUnprefixedString : UserHandle -> String 74 | toUnprefixedString (UserHandle slug) = 75 | Slug.toString slug 76 | 77 | 78 | equals : UserHandle -> UserHandle -> Bool 79 | equals (UserHandle a) (UserHandle b) = 80 | a == b 81 | 82 | 83 | 84 | -- DECODE 85 | 86 | 87 | decode : Decode.Decoder UserHandle 88 | decode = 89 | let 90 | decodeUserHandle_ s = 91 | case fromString s of 92 | Just u -> 93 | Decode.succeed u 94 | 95 | Nothing -> 96 | Decode.fail "Could not parse as UserHandle" 97 | in 98 | Decode.andThen decodeUserHandle_ string 99 | 100 | 101 | decodeUnprefixed : Decode.Decoder UserHandle 102 | decodeUnprefixed = 103 | let 104 | decodeUserHandle_ s = 105 | case fromUnprefixedString s of 106 | Just u -> 107 | Decode.succeed u 108 | 109 | Nothing -> 110 | Decode.fail "Could not parse as UserHandle" 111 | in 112 | Decode.andThen decodeUserHandle_ string 113 | -------------------------------------------------------------------------------- /src/Lib/Util.elm: -------------------------------------------------------------------------------- 1 | module Lib.Util exposing (..) 2 | 3 | import Http 4 | import Process 5 | import Task 6 | 7 | 8 | 9 | -- Various utility functions and helpers 10 | 11 | 12 | delayMsg : Float -> msg -> Cmd msg 13 | delayMsg delay msg = 14 | Task.perform (\_ -> msg) (Process.sleep delay) 15 | 16 | 17 | pipeMaybe : (b -> a -> a) -> Maybe b -> a -> a 18 | pipeMaybe f may a = 19 | case may of 20 | Just b -> 21 | f b a 22 | 23 | Nothing -> 24 | a 25 | 26 | 27 | pipeIf : (a -> a) -> Bool -> a -> a 28 | pipeIf f cond a = 29 | if cond then 30 | f a 31 | 32 | else 33 | a 34 | 35 | 36 | httpErrorToString : Http.Error -> String 37 | httpErrorToString err = 38 | case err of 39 | Http.Timeout -> 40 | "Timeout exceeded" 41 | 42 | Http.NetworkError -> 43 | "Network error" 44 | 45 | Http.BadStatus status -> 46 | "Bad status: " ++ String.fromInt status 47 | 48 | Http.BadBody text -> 49 | "Unexpected response from api: " ++ text 50 | 51 | Http.BadUrl url -> 52 | "Malformed url: " ++ url 53 | 54 | 55 | ascending : (a -> a -> Order) -> a -> a -> Order 56 | ascending comp a b = 57 | case comp a b of 58 | LT -> 59 | LT 60 | 61 | EQ -> 62 | EQ 63 | 64 | GT -> 65 | GT 66 | 67 | 68 | descending : (a -> a -> Order) -> a -> a -> Order 69 | descending comp a b = 70 | case comp a b of 71 | LT -> 72 | GT 73 | 74 | EQ -> 75 | EQ 76 | 77 | GT -> 78 | LT 79 | 80 | 81 | sortByWith : (x -> a) -> (a -> a -> Order) -> List x -> List x 82 | sortByWith get compare_ list = 83 | List.sortWith (\x1 x2 -> compare_ (get x1) (get x2)) list 84 | -------------------------------------------------------------------------------- /src/Lib/detectOs.d.ts: -------------------------------------------------------------------------------- 1 | declare function detectOs(nav: Navigator): string; 2 | -------------------------------------------------------------------------------- /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 | const nodeName = ev.target.nodeName; 16 | const isNotInput = nodeName !== "INPUT" && nodeName !== "TEXTAREA"; 17 | 18 | if ( 19 | (isNotInput && ev.key === "/") || 20 | (ev.metaKey && ev.key === "k") || 21 | (ev.ctrlKey && ev.key === "k") 22 | ) { 23 | ev.preventDefault(); 24 | } 25 | }); 26 | } 27 | 28 | export default preventDefaultGlobalKeyboardEvents; 29 | -------------------------------------------------------------------------------- /src/UI.elm: -------------------------------------------------------------------------------- 1 | module UI exposing (..) 2 | 3 | import Html exposing (Attribute, Html, code, div, pre, span, strong, text) 4 | import Html.Attributes exposing (class) 5 | import Html.Events exposing (onClick) 6 | import UI.Divider as Divider 7 | import UI.Icon as Icon 8 | 9 | 10 | codeBlock : List (Attribute msg) -> Html msg -> Html msg 11 | codeBlock attrs code_ = 12 | pre attrs [ code [] [ code_ ] ] 13 | 14 | 15 | bold : String -> Html msg 16 | bold text_ = 17 | strong [] [ text text_ ] 18 | 19 | 20 | inlineCode : List (Attribute msg) -> Html msg -> Html msg 21 | inlineCode attrs code_ = 22 | code (class "inline-code" :: attrs) [ code_ ] 23 | 24 | 25 | nothing : Html msg 26 | nothing = 27 | text "" 28 | 29 | 30 | viewMaybe : (a -> Html msg) -> Maybe a -> Html msg 31 | viewMaybe f a = 32 | case a of 33 | Just a_ -> 34 | f a_ 35 | 36 | Nothing -> 37 | nothing 38 | 39 | 40 | badge : Html msg -> Html msg 41 | badge content = 42 | span [ class "badge" ] [ content ] 43 | 44 | 45 | optionBadge : msg -> Html msg -> Html msg 46 | optionBadge removeMsg content = 47 | span [ class "option-badge", onClick removeMsg ] [ Icon.view Icon.x, content ] 48 | 49 | 50 | subtle : String -> Html msg 51 | subtle label = 52 | span [ class "subtle" ] [ text label ] 53 | 54 | 55 | loadingPlaceholder : Html msg 56 | loadingPlaceholder = 57 | div [ class "loading-placeholder" ] [] 58 | 59 | 60 | loadingPlaceholderRow : Html msg 61 | loadingPlaceholderRow = 62 | div [ class "loading-placeholder-row" ] 63 | [ div [ class "loading-placeholder" ] [] 64 | ] 65 | 66 | 67 | errorMessage : String -> Html msg 68 | errorMessage message = 69 | div [ class "error-message" ] [ text message ] 70 | 71 | 72 | emptyStateMessage : String -> Html msg 73 | emptyStateMessage message = 74 | div [ class "empty-state" ] [ text message ] 75 | 76 | 77 | divider : Html msg 78 | divider = 79 | Divider.divider |> Divider.view 80 | 81 | 82 | charWidth : Int -> String 83 | charWidth numChars = 84 | String.fromInt numChars ++ "ch" 85 | -------------------------------------------------------------------------------- /src/UI/AppDocument.elm: -------------------------------------------------------------------------------- 1 | module UI.AppDocument exposing (AppDocument, appDocument, map, view, view_, withAnnouncement) 2 | 3 | import Browser exposing (Document) 4 | import Html exposing (Html, div) 5 | import Html.Attributes exposing (class, id) 6 | import Maybe.Extra as MaybeE 7 | import UI 8 | import UI.AppHeader as AppHeader exposing (AppHeader) 9 | import UI.PageHeader as PageHeader exposing (PageHeader) 10 | 11 | 12 | 13 | {- 14 | 15 | AppDocument 16 | =========== 17 | 18 | Very similar to Browser.Document, but includes a common app title and app 19 | frame, as well as slots for header, page, and modals. 20 | -} 21 | 22 | 23 | type alias AppDocument msg = 24 | { pageId : String 25 | , title : String 26 | , announcement : Maybe (Html msg) 27 | , appHeader : AppHeader msg 28 | , pageHeader : Maybe (PageHeader msg) 29 | , page : Html msg 30 | , modal : Maybe (Html msg) 31 | } 32 | 33 | 34 | 35 | -- CREATE 36 | 37 | 38 | appDocument : String -> String -> AppHeader msg -> Html msg -> AppDocument msg 39 | appDocument pageId title appHeader page = 40 | { pageId = pageId 41 | , title = title 42 | , announcement = Nothing 43 | , appHeader = appHeader 44 | , pageHeader = Nothing 45 | , page = page 46 | , modal = Nothing 47 | } 48 | 49 | 50 | 51 | -- MAP 52 | 53 | 54 | map : (msgA -> msgB) -> AppDocument msgA -> AppDocument msgB 55 | map toMsgB { pageId, title, announcement, appHeader, pageHeader, page, modal } = 56 | { pageId = pageId 57 | , title = title 58 | , announcement = Maybe.map (Html.map toMsgB) announcement 59 | , appHeader = AppHeader.map toMsgB appHeader 60 | , pageHeader = Maybe.map (PageHeader.map toMsgB) pageHeader 61 | , page = Html.map toMsgB page 62 | , modal = Maybe.map (Html.map toMsgB) modal 63 | } 64 | 65 | 66 | withAnnouncement : Html msg -> AppDocument msg -> AppDocument msg 67 | withAnnouncement announcement appDoc = 68 | { appDoc | announcement = Just announcement } 69 | 70 | 71 | 72 | -- VIEW 73 | 74 | 75 | viewAnnouncement : Html msg -> Html msg 76 | viewAnnouncement content = 77 | div [ id "announcement" ] [ content ] 78 | 79 | 80 | view : AppDocument msg -> Document msg 81 | view appDoc = 82 | view_ appDoc [] 83 | 84 | 85 | view_ : AppDocument msg -> List (Html msg) -> Document msg 86 | view_ { pageId, title, announcement, appHeader, pageHeader, page, modal } extra = 87 | { title = title ++ " | Unison Share" 88 | , body = 89 | div 90 | [ id "app" 91 | , class pageId 92 | ] 93 | [ MaybeE.unwrap UI.nothing viewAnnouncement announcement 94 | , AppHeader.view appHeader 95 | , MaybeE.unwrap UI.nothing PageHeader.view pageHeader 96 | , page 97 | , Maybe.withDefault UI.nothing modal 98 | ] 99 | :: extra 100 | } 101 | -------------------------------------------------------------------------------- /src/UI/AvatarStack.elm: -------------------------------------------------------------------------------- 1 | module UI.AvatarStack exposing (..) 2 | 3 | import Html exposing (Html, div) 4 | import Html.Attributes exposing (class) 5 | import UI.Avatar as Avatar exposing (Avatar) 6 | 7 | 8 | view : List (Avatar msg) -> Html msg 9 | view avatars = 10 | div [ class "avatar-stack" ] (List.map Avatar.view avatars) 11 | -------------------------------------------------------------------------------- /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/ByAt.elm: -------------------------------------------------------------------------------- 1 | {- 2 | ByAt 3 | ==== 4 | 5 | A small module to help consistently render when a user did something. 6 | 7 | For example: 8 | Release 2.1.3 - (H) @hopper 2 days ago 9 | ╰──────────┬───────────╯ 10 | This module renders that 11 | -} 12 | 13 | 14 | module UI.ByAt exposing (ByAt, byAt, byUnknown, view, withToClick) 15 | 16 | import Html exposing (Html, div, span, strong, text) 17 | import Html.Attributes exposing (class) 18 | import Lib.UserHandle as UserHandle exposing (UserHandle) 19 | import Time 20 | import UI.Click as Click exposing (Click) 21 | import UI.DateTime as DateTime exposing (DateTime) 22 | import UI.ProfileSnippet as ProfileSnippet 23 | import Url exposing (Url) 24 | 25 | 26 | type alias User u = 27 | { u 28 | | handle : UserHandle 29 | , name : Maybe String 30 | , avatarUrl : Maybe Url 31 | } 32 | 33 | 34 | type By u 35 | = ByUser (User u) 36 | | ByHandle UserHandle 37 | | ByUnknown 38 | 39 | 40 | type ByAt u msg 41 | = ByAt 42 | { by : By u 43 | , at : DateTime 44 | , toClick : Maybe (UserHandle -> Click msg) 45 | } 46 | 47 | 48 | 49 | -- CREATE 50 | 51 | 52 | byAt : User u -> DateTime -> ByAt u msg 53 | byAt by at = 54 | ByAt { by = ByUser by, at = at, toClick = Nothing } 55 | 56 | 57 | byUnknown : DateTime -> ByAt u msg 58 | byUnknown at = 59 | ByAt { by = ByUnknown, at = at, toClick = Nothing } 60 | 61 | 62 | 63 | -- MODIFY 64 | 65 | 66 | withToClick : (UserHandle -> Click msg) -> ByAt u msg -> ByAt u msg 67 | withToClick toClick (ByAt byAt_) = 68 | ByAt { byAt_ | toClick = Just toClick } 69 | 70 | 71 | 72 | -- VIEW 73 | 74 | 75 | view : Time.Zone -> DateTime -> ByAt u msg -> Html msg 76 | view zone now (ByAt { by, at, toClick }) = 77 | let 78 | ( profileSnippet, click_ ) = 79 | case by of 80 | ByUser u -> 81 | ( u 82 | |> ProfileSnippet.profileSnippet 83 | |> ProfileSnippet.small 84 | |> ProfileSnippet.view 85 | , Maybe.map (\f -> f u.handle) toClick 86 | ) 87 | 88 | ByHandle h -> 89 | ( strong [] [ text (UserHandle.toString h) ], Maybe.map (\f -> f h) toClick ) 90 | 91 | ByUnknown -> 92 | ( strong [] [ text "Unknown user" ], Nothing ) 93 | 94 | attrs = 95 | [ class "by-at" ] 96 | 97 | content = 98 | [ profileSnippet 99 | , span [ class "by-at_at" ] 100 | [ DateTime.view (DateTime.DistanceFrom now) zone at ] 101 | ] 102 | in 103 | case click_ of 104 | Nothing -> 105 | div attrs content 106 | 107 | Just c -> 108 | Click.view attrs content c 109 | -------------------------------------------------------------------------------- /src/UI/CopyField.elm: -------------------------------------------------------------------------------- 1 | module UI.CopyField exposing (..) 2 | 3 | import Html exposing (Html, div, input, text) 4 | import Html.Attributes exposing (class, readonly, type_, value) 5 | import UI 6 | import UI.CopyOnClick as CopyOnClick 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 | , CopyOnClick.copyButton_ field.onCopy field.toCopy 54 | ] 55 | -------------------------------------------------------------------------------- /src/UI/CopyOnClick.elm: -------------------------------------------------------------------------------- 1 | module UI.CopyOnClick exposing (..) 2 | 3 | import Html exposing (Attribute, Html, button, div, node) 4 | import Html.Attributes exposing (attribute, class) 5 | import Html.Events exposing (on) 6 | import Json.Decode as Decode 7 | import UI.Icon as Icon 8 | 9 | 10 | onCopy : (String -> msg) -> Attribute msg 11 | onCopy msg = 12 | Decode.map msg Decode.string 13 | |> on "copy" 14 | 15 | 16 | view : String -> Html msg -> Html msg -> Html msg 17 | view toCopy trigger success = 18 | node "copy-on-click" 19 | [ class "copy-on-click", attribute "text" toCopy ] 20 | [ trigger, div [ class "copy-on-click_success" ] [ success ] ] 21 | 22 | 23 | view_ : (String -> msg) -> String -> Html msg -> Html msg -> Html msg 24 | view_ onCopyMsg toCopy trigger success = 25 | node "copy-on-click" 26 | [ class "copy-on-click", onCopy onCopyMsg, attribute "text" toCopy ] 27 | [ trigger, div [ class "copy-on-click_success" ] [ success ] ] 28 | 29 | 30 | {-| We're not using UI.Button here since a click handler is added from 31 | the webcomponent in JS land. 32 | -} 33 | copyButton : String -> Html msg 34 | copyButton toCopy = 35 | view 36 | toCopy 37 | (button [ class "button contained default" ] [ Icon.view Icon.clipboard ]) 38 | (div [ class "copy-field_success" ] [ Icon.view Icon.checkmark ]) 39 | 40 | 41 | copyButton_ : (String -> msg) -> String -> Html msg 42 | copyButton_ onCopyMsg toCopy = 43 | view_ onCopyMsg 44 | toCopy 45 | (button [ class "button contained default" ] [ Icon.view Icon.clipboard ]) 46 | (div [ class "copy-field_success" ] [ Icon.view Icon.checkmark ]) 47 | -------------------------------------------------------------------------------- /src/UI/CopyOnClick.js: -------------------------------------------------------------------------------- 1 | // 2 | // clickable content 3 | // 4 | class CopyOnClick extends HTMLElement { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | connectedCallback() { 10 | this.addEventListener("click", () => { 11 | const text = this.getAttribute("text"); 12 | 13 | // writeText returns a promise with success/failure that we should 14 | // probably do something with... 15 | navigator.clipboard.writeText(text); 16 | this.classList.add("copy-success"); 17 | setTimeout(() => { 18 | this.classList.remove("copy-success"); 19 | }, 1500); 20 | }); 21 | } 22 | 23 | static get observedAttributes() { 24 | return ["text"]; 25 | } 26 | } 27 | 28 | customElements.define("copy-on-click", CopyOnClick); 29 | -------------------------------------------------------------------------------- /src/UI/CopyrightYear.elm: -------------------------------------------------------------------------------- 1 | {- Displays the copyright symbol and the current year: "© 2023" -} 2 | 3 | 4 | module UI.CopyrightYear exposing (..) 5 | 6 | import Html exposing (Html, node) 7 | 8 | 9 | view : Html msg 10 | view = 11 | node "copyright-year" [] [] 12 | -------------------------------------------------------------------------------- /src/UI/CopyrightYear.js: -------------------------------------------------------------------------------- 1 | // Displays the copyright symbol and the current year. 2 | // example: 3 | // 4 | // 5 | // ⧨ 6 | // 7 | // © 2023 8 | // 9 | // 10 | class CopyrightYear extends HTMLElement { 11 | constructor() { 12 | super(); 13 | } 14 | 15 | connectedCallback() { 16 | const d = new Date(); 17 | this.innerText = `© ${d.getFullYear()}`; 18 | this.classList.add("copyright-year"); 19 | } 20 | } 21 | 22 | customElements.define("copyright-year", CopyrightYear); 23 | -------------------------------------------------------------------------------- /src/UI/Divider.elm: -------------------------------------------------------------------------------- 1 | module UI.Divider exposing (..) 2 | 3 | import Html exposing (Html, hr) 4 | import Html.Attributes exposing (class) 5 | 6 | 7 | type OnSurface 8 | = Dark 9 | | Light 10 | 11 | 12 | type DividerSize 13 | = Normal 14 | | Small 15 | 16 | 17 | type alias Divider = 18 | { onSurface : OnSurface 19 | , size : DividerSize 20 | , margin : Bool 21 | } 22 | 23 | 24 | 25 | -- CREATE 26 | 27 | 28 | divider : Divider 29 | divider = 30 | { onSurface = Light, size = Normal, margin = True } 31 | 32 | 33 | 34 | -- MODIFY 35 | 36 | 37 | withOnSurface : OnSurface -> Divider -> Divider 38 | withOnSurface onSurface d = 39 | { d | onSurface = onSurface } 40 | 41 | 42 | onDark : Divider -> Divider 43 | onDark d = 44 | withOnSurface Dark d 45 | 46 | 47 | onLight : Divider -> Divider 48 | onLight d = 49 | withOnSurface Light d 50 | 51 | 52 | withSize : DividerSize -> Divider -> Divider 53 | withSize size d = 54 | { d | size = size } 55 | 56 | 57 | small : Divider -> Divider 58 | small d = 59 | withSize Small d 60 | 61 | 62 | withMargin : Divider -> Divider 63 | withMargin d = 64 | { d | margin = True } 65 | 66 | 67 | withoutMargin : Divider -> Divider 68 | withoutMargin d = 69 | { d | margin = False } 70 | 71 | 72 | 73 | -- VIEW 74 | 75 | 76 | view : Divider -> Html msg 77 | view d = 78 | let 79 | sizeClass = 80 | case d.size of 81 | Small -> 82 | class "divider_size_small" 83 | 84 | Normal -> 85 | class "divider_size_normal" 86 | 87 | surfaceClass = 88 | case d.onSurface of 89 | Light -> 90 | class "divider_on-surface_light" 91 | 92 | Dark -> 93 | class "divider_on-surface_dark" 94 | 95 | paddingClass = 96 | if d.margin then 97 | class "divider_margin" 98 | 99 | else 100 | class "divider_no-margin" 101 | in 102 | hr [ class "divider", sizeClass, surfaceClass, paddingClass ] [] 103 | -------------------------------------------------------------------------------- /src/UI/EmptyStateCard.elm: -------------------------------------------------------------------------------- 1 | module UI.EmptyStateCard exposing (..) 2 | 3 | import Html exposing (Html, div) 4 | import Html.Attributes exposing (class) 5 | import UI.Card as Card exposing (Card) 6 | import UI.EmptyState as EmptyState exposing (EmptyState) 7 | 8 | 9 | 10 | -- VIEW 11 | 12 | 13 | asCard : EmptyState msg -> Card msg 14 | asCard emptyState = 15 | Card.card 16 | [ div [ class "empty-state-card" ] [ EmptyState.view emptyState ] 17 | ] 18 | |> Card.asContainedWithFade 19 | 20 | 21 | asCard_ : Card.SurfaceBackgroundColor -> EmptyState msg -> Card msg 22 | asCard_ surfaceBackground emptyState = 23 | Card.card 24 | [ div [ class "empty-state-card" ] [ EmptyState.view emptyState ] 25 | ] 26 | |> Card.asContainedWithFade_ surfaceBackground 27 | 28 | 29 | view : EmptyState msg -> Html msg 30 | view = 31 | asCard >> Card.view 32 | 33 | 34 | view_ : Card.SurfaceBackgroundColor -> EmptyState msg -> Html msg 35 | view_ surfaceBackground emptyState = 36 | emptyState |> asCard_ surfaceBackground |> Card.view 37 | -------------------------------------------------------------------------------- /src/UI/ErrorCard.elm: -------------------------------------------------------------------------------- 1 | module UI.ErrorCard exposing (..) 2 | 3 | import Html exposing (Html, div, h2, p, text) 4 | import Html.Attributes exposing (class) 5 | import Maybe.Extra as MaybeE 6 | import UI 7 | import UI.Button as Button exposing (Button) 8 | import UI.Card as Card exposing (Card) 9 | 10 | 11 | type alias ErrorCard msg = 12 | { title : Maybe String 13 | , text : Maybe String 14 | , action : Maybe (Button msg) 15 | } 16 | 17 | 18 | 19 | -- CREATE 20 | 21 | 22 | empty : ErrorCard msg 23 | empty = 24 | { title = Nothing, text = Nothing, action = Nothing } 25 | 26 | 27 | errorCard : String -> String -> ErrorCard msg 28 | errorCard title txt = 29 | { title = Just title, text = Just txt, action = Nothing } 30 | 31 | 32 | errorCard_ : String -> String -> Button msg -> ErrorCard msg 33 | errorCard_ title txt action = 34 | { title = Just title, text = Just txt, action = Just action } 35 | 36 | 37 | 38 | -- MODIFY 39 | 40 | 41 | withText : String -> ErrorCard msg -> ErrorCard msg 42 | withText txt errCard = 43 | { errCard | text = Just txt } 44 | 45 | 46 | withTitle : String -> ErrorCard msg -> ErrorCard msg 47 | withTitle title errCard = 48 | { errCard | title = Just title } 49 | 50 | 51 | withAction : Button msg -> ErrorCard msg -> ErrorCard msg 52 | withAction action errCard = 53 | { errCard | action = Just action } 54 | 55 | 56 | 57 | -- TRANSFORM 58 | 59 | 60 | toCard : ErrorCard msg -> Card msg 61 | toCard errCard = 62 | let 63 | title = 64 | Maybe.withDefault "Oh no!" errCard.title 65 | 66 | txt = 67 | Maybe.withDefault "We're terribly sorry, but something unexpected happened with this page and we couldn't display it." errCard.text 68 | in 69 | Card.card 70 | [ div [ class "error-card" ] 71 | [ div [ class "emoji" ] [ text "😞" ] 72 | , div [ class "error-card_details" ] 73 | [ h2 [] [ text title ] 74 | , p [] [ text txt ] 75 | ] 76 | , MaybeE.unwrap UI.nothing Button.view errCard.action 77 | ] 78 | ] 79 | 80 | 81 | 82 | -- VIEW 83 | 84 | 85 | view : ErrorCard msg -> Html msg 86 | view = 87 | toCard >> Card.view 88 | -------------------------------------------------------------------------------- /src/UI/ExternalLinkIcon.elm: -------------------------------------------------------------------------------- 1 | module UI.ExternalLinkIcon exposing (view) 2 | 3 | import Html exposing (Html) 4 | import Html.Attributes exposing (class) 5 | import UI.Click as Click exposing (Click) 6 | import UI.Icon as Icon 7 | 8 | 9 | view : Click msg -> Html msg 10 | view click = 11 | Click.view [ class "external-link-icon" ] 12 | [ Icon.view Icon.arrowEscapeBox ] 13 | click 14 | -------------------------------------------------------------------------------- /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/Form/Checkbox.elm: -------------------------------------------------------------------------------- 1 | module UI.Form.Checkbox exposing (..) 2 | 3 | import Html exposing (Html, div, input) 4 | import Html.Attributes exposing (checked, class, type_) 5 | import UI.Click as Click 6 | 7 | 8 | type alias Checkbox msg = 9 | { onChange : Maybe msg 10 | , checked : Bool 11 | } 12 | 13 | 14 | 15 | -- CREATE 16 | 17 | 18 | checkbox : msg -> Bool -> Checkbox msg 19 | checkbox onChange checked = 20 | checkbox_ (Just onChange) checked 21 | 22 | 23 | checkbox_ : Maybe msg -> Bool -> Checkbox msg 24 | checkbox_ onChange checked = 25 | Checkbox onChange checked 26 | 27 | 28 | 29 | -- VIEW 30 | 31 | 32 | view : Checkbox msg -> Html msg 33 | view box = 34 | case box.onChange of 35 | Just onChange -> 36 | Click.view [ class "checkbox" ] 37 | [ input [ type_ "checkbox", checked box.checked ] [] ] 38 | (Click.onClick onChange) 39 | 40 | Nothing -> 41 | div [ class "checkbox" ] 42 | [ input [ type_ "checkbox", checked box.checked ] [] 43 | ] 44 | -------------------------------------------------------------------------------- /src/UI/Form/CheckboxField.elm: -------------------------------------------------------------------------------- 1 | module UI.Form.CheckboxField exposing (..) 2 | 3 | import Html exposing (Html, div, label, small, text) 4 | import Html.Attributes exposing (class) 5 | import UI.Click as Click 6 | import UI.Form.Checkbox as Checkbox 7 | 8 | 9 | type alias CheckboxField msg = 10 | { label : String 11 | , helpText : Maybe String 12 | , onChange : msg 13 | , checked : Bool 14 | } 15 | 16 | 17 | 18 | -- CREATE 19 | 20 | 21 | field : String -> msg -> Bool -> CheckboxField msg 22 | field label onChange checked = 23 | CheckboxField label Nothing onChange checked 24 | 25 | 26 | 27 | -- MODIFY 28 | 29 | 30 | withHelpText : String -> CheckboxField msg -> CheckboxField msg 31 | withHelpText helpText field_ = 32 | { field_ | helpText = Just helpText } 33 | 34 | 35 | 36 | -- VIEW 37 | 38 | 39 | view : CheckboxField msg -> Html msg 40 | view checkboxField = 41 | let 42 | labelAndHelpText = 43 | case checkboxField.helpText of 44 | Just ht -> 45 | div 46 | [ class "label-and-help-text" ] 47 | [ label [ class "label" ] [ text checkboxField.label ] 48 | , small [ class "help-text" ] [ text ht ] 49 | ] 50 | 51 | Nothing -> 52 | label [ class "label" ] [ text checkboxField.label ] 53 | in 54 | Click.view [ class "form-field checkbox-field" ] 55 | [ Checkbox.checkbox_ Nothing checkboxField.checked |> Checkbox.view 56 | , labelAndHelpText 57 | ] 58 | (Click.onClick checkboxField.onChange) 59 | -------------------------------------------------------------------------------- /src/UI/ModalOverlay.elm: -------------------------------------------------------------------------------- 1 | module UI.ModalOverlay exposing (modalOverlay) 2 | 3 | import Html exposing (Html, node) 4 | import Html.Events exposing (on) 5 | import Json.Decode as Decode 6 | 7 | 8 | modalOverlay : Maybe msg -> Html msg -> Html msg 9 | modalOverlay onEscape content = 10 | let 11 | attrs = 12 | case onEscape of 13 | Just onEsc -> 14 | [ on "escape" (Decode.succeed onEsc) ] 15 | 16 | Nothing -> 17 | [] 18 | in 19 | node "modal-overlay" attrs [ content ] 20 | -------------------------------------------------------------------------------- /src/UI/ModalOverlay.js: -------------------------------------------------------------------------------- 1 | // Common modal WebComponent for both AnchoredOverlay and Modal 2 | // which adds keyboard shortcuts. 3 | // 4 | // 5 | // modal content 6 | // 7 | class ModalOverlay extends HTMLElement { 8 | static get observedAttributes() { 9 | return ["text"]; 10 | } 11 | 12 | constructor() { 13 | super(); 14 | } 15 | 16 | connectedCallback() { 17 | this.setFocus(); 18 | 19 | // TODO, handle nested overlays better. Like if an AnchoredOverlay is 20 | // inside of a Modal, we'd want the AnchoredOverlay to be dismissed, not 21 | // the modal (unless no AnchoredOverlay is open) 22 | this.onKeydown = (ev) => { 23 | // If the element isn't visible, there's no point in triggering the escape event. 24 | const style = window.getComputedStyle(this); 25 | if (style.display === "none" || this.offsetParent === null) { 26 | return; 27 | } 28 | 29 | if (ev.key === "Escape") { 30 | ev.preventDefault(); 31 | ev.stopPropagation(); 32 | 33 | this.dispatchEvent(new CustomEvent("escape")); 34 | } 35 | }; 36 | 37 | window.addEventListener("keydown", this.onKeydown); 38 | } 39 | 40 | disconnectedCallback() { 41 | window.removeEventListener("keydown", this.onKeydown); 42 | } 43 | 44 | setFocus() { 45 | // Autofocus only actually work on page load, so we check for its existence 46 | // in modals and trigger focus. 47 | const autofocused = this.querySelector("[autofocus]"); 48 | if (autofocused) { 49 | autofocused.focus(); 50 | } else { 51 | const focusClassName = this.getAttribute("focusClassName"); 52 | if (focusClassName) { 53 | this.querySelector(focusClassName)?.focus(); 54 | } else { 55 | this.firstChild?.focus(); 56 | } 57 | } 58 | } 59 | } 60 | 61 | customElements.define("modal-overlay", ModalOverlay); 62 | -------------------------------------------------------------------------------- /src/UI/OnSurface.elm: -------------------------------------------------------------------------------- 1 | module UI.OnSurface exposing (..) 2 | 3 | import Html 4 | import Html.Attributes exposing (class) 5 | 6 | 7 | type OnSurface 8 | = Dark 9 | | Light 10 | 11 | 12 | dark : OnSurface 13 | dark = 14 | Dark 15 | 16 | 17 | light : OnSurface 18 | light = 19 | Light 20 | 21 | 22 | toClassName : OnSurface -> String 23 | toClassName onSurface = 24 | case onSurface of 25 | Light -> 26 | "on-light" 27 | 28 | Dark -> 29 | "on-dark" 30 | 31 | 32 | toClass : OnSurface -> Html.Attribute msg 33 | toClass = 34 | toClassName >> class 35 | -------------------------------------------------------------------------------- /src/UI/README.md: -------------------------------------------------------------------------------- 1 | # Unison Design System 2 | 3 | Fonts, Colors, and Components. 4 | -------------------------------------------------------------------------------- /src/UI/Sizing.elm: -------------------------------------------------------------------------------- 1 | module UI.Sizing exposing (..) 2 | 3 | 4 | type Rem 5 | = Rem Float 6 | 7 | 8 | times : Int -> Rem -> Rem 9 | times i (Rem r) = 10 | Rem (toFloat i * r) 11 | 12 | 13 | multiply : Rem -> Rem -> Rem 14 | multiply (Rem a) (Rem b) = 15 | Rem (a * b) 16 | 17 | 18 | add : Rem -> Rem -> Rem 19 | add (Rem a) (Rem b) = 20 | Rem (a + b) 21 | 22 | 23 | subtract : Rem -> Rem -> Rem 24 | subtract (Rem a) (Rem b) = 25 | Rem (a - b) 26 | 27 | 28 | lt : Rem -> Rem -> Bool 29 | lt (Rem a) (Rem b) = 30 | a < b 31 | 32 | 33 | lteq : Rem -> Rem -> Bool 34 | lteq (Rem a) (Rem b) = 35 | a <= b 36 | 37 | 38 | gteq : Rem -> Rem -> Bool 39 | gteq (Rem a) (Rem b) = 40 | a >= b 41 | 42 | 43 | gt : Rem -> Rem -> Bool 44 | gt (Rem a) (Rem b) = 45 | a > b 46 | 47 | 48 | equals : Rem -> Rem -> Bool 49 | equals (Rem a) (Rem b) = 50 | a == b 51 | 52 | 53 | fromPx : Int -> Rem 54 | fromPx px = 55 | Rem (toFloat oneRemInPx / toFloat px) 56 | 57 | 58 | oneRemInPx : Int 59 | oneRemInPx = 60 | 16 61 | 62 | 63 | toPx : Rem -> Int 64 | toPx (Rem r) = 65 | floor (r * toFloat oneRemInPx) 66 | -------------------------------------------------------------------------------- /src/UI/StatusBanner.elm: -------------------------------------------------------------------------------- 1 | module UI.StatusBanner exposing (..) 2 | 3 | import Html exposing (Html, div, p, text) 4 | import Html.Attributes exposing (class) 5 | import UI.StatusIndicator as StatusIndicator 6 | 7 | 8 | type StatusBanner msg 9 | = Good (Html msg) 10 | | Bad (Html msg) 11 | | Info (Html msg) 12 | | Working (Html msg) 13 | 14 | 15 | good : String -> Html msg 16 | good text_ = 17 | good_ (multilined text_) 18 | 19 | 20 | good_ : Html msg -> Html msg 21 | good_ content = 22 | view (Good content) 23 | 24 | 25 | bad : String -> Html msg 26 | bad text_ = 27 | bad_ (multilined text_) 28 | 29 | 30 | bad_ : Html msg -> Html msg 31 | bad_ content = 32 | view (Bad content) 33 | 34 | 35 | info : String -> Html msg 36 | info text_ = 37 | info_ (multilined text_) 38 | 39 | 40 | info_ : Html msg -> Html msg 41 | info_ content = 42 | view (Info content) 43 | 44 | 45 | working : String -> Html msg 46 | working text_ = 47 | working_ (multilined text_) 48 | 49 | 50 | working_ : Html msg -> Html msg 51 | working_ content = 52 | view (Working content) 53 | 54 | 55 | multilined : String -> Html msg 56 | multilined text_ = 57 | text_ 58 | |> String.split "\n" 59 | |> List.map (\t -> p [] [ text t ]) 60 | |> div [] 61 | 62 | 63 | view : StatusBanner msg -> Html msg 64 | view banner = 65 | let 66 | ( className, indicator, content ) = 67 | case banner of 68 | Good c -> 69 | ( "good", StatusIndicator.good, c ) 70 | 71 | Bad c -> 72 | ( "bad", StatusIndicator.bad, c ) 73 | 74 | Info c -> 75 | ( "info", StatusIndicator.info, c ) 76 | 77 | Working c -> 78 | ( "working", StatusIndicator.working, c ) 79 | in 80 | div [ class ("status-banner status-banner_" ++ className) ] 81 | [ indicator |> StatusIndicator.view 82 | , div [ class "status-banner_content" ] [ content ] 83 | ] 84 | -------------------------------------------------------------------------------- /src/UI/StatusIndicator.elm: -------------------------------------------------------------------------------- 1 | module UI.StatusIndicator exposing (..) 2 | 3 | import Html exposing (Html, div) 4 | import Html.Attributes exposing (class) 5 | import UI.Icon as Icon 6 | 7 | 8 | type Indicator 9 | = Good 10 | | Bad 11 | | Info 12 | | Working 13 | 14 | 15 | type Size 16 | = Regular 17 | | Large 18 | 19 | 20 | type alias StatusIndicator = 21 | { indicator : Indicator 22 | , size : Size 23 | } 24 | 25 | 26 | 27 | -- CREATE 28 | 29 | 30 | good : StatusIndicator 31 | good = 32 | { indicator = Good, size = Regular } 33 | 34 | 35 | bad : StatusIndicator 36 | bad = 37 | { indicator = Bad, size = Regular } 38 | 39 | 40 | info : StatusIndicator 41 | info = 42 | { indicator = Info, size = Regular } 43 | 44 | 45 | working : StatusIndicator 46 | working = 47 | { indicator = Working, size = Regular } 48 | 49 | 50 | 51 | -- MODIFY 52 | 53 | 54 | withSize : Size -> StatusIndicator -> StatusIndicator 55 | withSize size statusIndicator = 56 | { statusIndicator | size = size } 57 | 58 | 59 | large : StatusIndicator -> StatusIndicator 60 | large statusIndicator = 61 | withSize Large statusIndicator 62 | 63 | 64 | regular : StatusIndicator -> StatusIndicator 65 | regular statusIndicator = 66 | withSize Regular statusIndicator 67 | 68 | 69 | 70 | -- VIEW 71 | 72 | 73 | view : StatusIndicator -> Html msg 74 | view { indicator, size } = 75 | let 76 | ( className, content ) = 77 | case indicator of 78 | Good -> 79 | ( "good", Icon.view Icon.checkmark ) 80 | 81 | Bad -> 82 | ( "bad", Icon.view Icon.warn ) 83 | 84 | Info -> 85 | ( "info", Icon.view Icon.bulb ) 86 | 87 | Working -> 88 | ( "working", Icon.view Icon.largeDot ) 89 | 90 | sizeClassName = 91 | case size of 92 | Regular -> 93 | "status-indicator_regular" 94 | 95 | Large -> 96 | "status-indicator_large" 97 | in 98 | div [ class ("status-indicator status-indicator_" ++ className ++ " " ++ sizeClassName) ] 99 | [ content ] 100 | -------------------------------------------------------------------------------- /src/UI/StatusMessage.elm: -------------------------------------------------------------------------------- 1 | module UI.StatusMessage exposing (..) 2 | 3 | import Html exposing (Html, div, h2, text) 4 | import Html.Attributes exposing (class) 5 | import Maybe.Extra as MaybeE 6 | import UI 7 | import UI.Button as Button exposing (Button) 8 | import UI.Card as Card exposing (Card) 9 | import UI.StatusIndicator as StatusIndicator exposing (StatusIndicator) 10 | 11 | 12 | type alias StatusMessage msg = 13 | { status : StatusIndicator 14 | , title : String 15 | , body : List (Html msg) 16 | , cta : Maybe (Button msg) 17 | } 18 | 19 | 20 | good : String -> List (Html msg) -> StatusMessage msg 21 | good title body = 22 | { status = StatusIndicator.good 23 | , title = title 24 | , body = body 25 | , cta = Nothing 26 | } 27 | 28 | 29 | bad : String -> List (Html msg) -> StatusMessage msg 30 | bad title body = 31 | { status = StatusIndicator.bad 32 | , title = title 33 | , body = body 34 | , cta = Nothing 35 | } 36 | 37 | 38 | 39 | -- MODIFY 40 | 41 | 42 | withCta : Button msg -> StatusMessage msg -> StatusMessage msg 43 | withCta cta statusMessage = 44 | { statusMessage | cta = Just cta } 45 | 46 | 47 | view : StatusMessage msg -> Html msg 48 | view { status, title, body, cta } = 49 | div [ class "status-message" ] 50 | ([ status |> StatusIndicator.large |> StatusIndicator.view 51 | , h2 [ class "status-message_title" ] [ text title ] 52 | ] 53 | ++ body 54 | ++ [ MaybeE.unwrap UI.nothing Button.view cta ] 55 | ) 56 | 57 | 58 | asCard : StatusMessage msg -> Card msg 59 | asCard statusMessage = 60 | Card.card [ view statusMessage ] 61 | -------------------------------------------------------------------------------- /src/UI/Steps.elm: -------------------------------------------------------------------------------- 1 | module UI.Steps exposing (Step, Steps, singleton, step, steps, view, withStep) 2 | 3 | import Html exposing (Html, div, h3, text) 4 | import Html.Attributes exposing (class) 5 | import List.Nonempty as Nonempty exposing (Nonempty) 6 | 7 | 8 | type alias Step msg = 9 | { title : String, content : List (Html msg) } 10 | 11 | 12 | type alias Steps msg = 13 | Nonempty (Step msg) 14 | 15 | 16 | 17 | -- CREATE 18 | 19 | 20 | step : String -> List (Html msg) -> Step msg 21 | step title content = 22 | { title = title, content = content } 23 | 24 | 25 | steps : Step msg -> List (Step msg) -> Steps msg 26 | steps step_ steps_ = 27 | Nonempty.Nonempty step_ steps_ 28 | 29 | 30 | singleton : Step msg -> Steps msg 31 | singleton step_ = 32 | Nonempty.fromElement step_ 33 | 34 | 35 | withStep : Step msg -> Steps msg -> Steps msg 36 | withStep step_ steps_ = 37 | Nonempty.append steps_ (Nonempty.singleton step_) 38 | 39 | 40 | 41 | -- VIEW 42 | 43 | 44 | viewStep : Int -> Step msg -> Html msg 45 | viewStep index step_ = 46 | let 47 | stepNumber = 48 | div [ class "step-number" ] [ text (String.fromInt (index + 1)) ] 49 | 50 | details = 51 | div [ class "step-details" ] 52 | [ h3 [ class "step-title" ] [ text step_.title ] 53 | , div [ class "step-content" ] step_.content 54 | ] 55 | in 56 | div [ class "step" ] 57 | [ stepNumber 58 | , details 59 | ] 60 | 61 | 62 | view : Steps msg -> Html msg 63 | view steps_ = 64 | let 65 | steps__ = 66 | steps_ |> Nonempty.toList |> List.indexedMap viewStep 67 | in 68 | div [ class "steps" ] steps__ 69 | -------------------------------------------------------------------------------- /src/UI/TabList.elm: -------------------------------------------------------------------------------- 1 | {- 2 | Tabs 3 | ==== 4 | 5 | (i) It would be prudent for this module to wrap UI.Navigation, or use it in 6 | some way, but we want something a little more closed and Navigation 7 | works with NavItems which can't be wrapped (Higher Kinded Types would 8 | have helped solve that). 9 | -} 10 | 11 | 12 | module UI.TabList exposing 13 | ( Tab 14 | , TabList 15 | , map 16 | , tab 17 | , tabList 18 | , view 19 | , viewTab 20 | ) 21 | 22 | import Html exposing (Html, nav, span, text) 23 | import Html.Attributes exposing (class) 24 | import Lib.Aria exposing (role) 25 | import List.Zipper as Zipper exposing (Zipper) 26 | import UI.Click as Click exposing (Click) 27 | 28 | 29 | type Tab msg 30 | = Tab { label : String, click : Click msg } 31 | 32 | 33 | type TabList msg 34 | = TabList (Zipper (Tab msg)) 35 | 36 | 37 | 38 | -- CREATE 39 | 40 | 41 | tab : String -> Click msg -> Tab msg 42 | tab label click = 43 | Tab { label = label, click = click } 44 | 45 | 46 | tabList : List (Tab msg) -> Tab msg -> List (Tab msg) -> TabList msg 47 | tabList before selected_ after = 48 | TabList (Zipper.from before selected_ after) 49 | 50 | 51 | 52 | -- MAP 53 | 54 | 55 | mapTab : (a -> b) -> Tab a -> Tab b 56 | mapTab f (Tab a) = 57 | Tab { label = a.label, click = Click.map f a.click } 58 | 59 | 60 | map : (a -> b) -> TabList a -> TabList b 61 | map f (TabList a) = 62 | TabList (Zipper.map (mapTab f) a) 63 | 64 | 65 | 66 | -- VIEW 67 | 68 | 69 | viewTab : Bool -> Tab msg -> Html msg 70 | viewTab isSelected (Tab tab_) = 71 | let 72 | attrs = 73 | [ class "tab", role "tab" ] 74 | 75 | content = 76 | [ text tab_.label ] 77 | in 78 | if isSelected then 79 | span (class "tab_selected" :: attrs) content 80 | 81 | else 82 | Click.view attrs content tab_.click 83 | 84 | 85 | view : TabList msg -> Html msg 86 | view (TabList tabList_) = 87 | let 88 | before = 89 | List.map (viewTab False) (Zipper.before tabList_) 90 | 91 | selected = 92 | viewTab True (Zipper.current tabList_) 93 | 94 | after = 95 | List.map (viewTab False) (Zipper.after tabList_) 96 | in 97 | nav [ class "tab-list", role "tablist" ] 98 | (before ++ (selected :: after)) 99 | -------------------------------------------------------------------------------- /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/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-name-listing.css"; 7 | @import "./code/project-name.css"; 8 | @import "./code/hashvatar.css"; 9 | @import "./code/empty-state.css"; 10 | @import "./code/hash.css"; 11 | @import "./code/version.css"; 12 | -------------------------------------------------------------------------------- /src/css/code/empty-state.css: -------------------------------------------------------------------------------- 1 | .code_empty-state { 2 | font-size: var(--font-size-medium); 3 | background: url(../ui/empty-state_grid.svg); 4 | background-position: center 2rem; 5 | background-repeat: no-repeat; 6 | background-size: contain; 7 | padding: 0rem 4.625rem 5.5rem 4.625rem; 8 | display: flex; 9 | justify-content: center; 10 | color: var(--u-color_text); 11 | width: 37.5rem; 12 | } 13 | 14 | .code_empty-state header { 15 | display: flex; 16 | flex-direction: column; 17 | gap: 0.5rem; 18 | } 19 | 20 | .code_empty-state h2 { 21 | font-size: 1.25rem; 22 | color: var(--u-color_text); 23 | } 24 | .code_empty-state p { 25 | margin-bottom: 0; 26 | } 27 | 28 | .code_empty-state .code_empty-state_content { 29 | flex: 1; 30 | display: flex; 31 | flex-direction: column; 32 | gap: 1.5rem; 33 | } 34 | 35 | .code_empty-state .code_empty-state_faux-definition { 36 | border: 1px solid var(--u-color_border); 37 | border-radius: var(--border-radius-base); 38 | background: var(--u-color_element); 39 | padding: 1rem 0.75rem; 40 | display: flex; 41 | flex-direction: column; 42 | gap: 0.875rem; 43 | } 44 | 45 | .code_empty-state .button { 46 | display: flex; 47 | align-self: flex-end; 48 | } 49 | 50 | @media only screen and (--u-viewport_max-lg) { 51 | .code_empty-state { 52 | width: auto; 53 | margin: auto; 54 | } 55 | } 56 | 57 | @media only screen and (--u-viewport_max-sm) { 58 | .code_empty-state { 59 | padding: 0 1rem 2rem 1rem; 60 | } 61 | 62 | .code_empty-state .placeholder-shape { 63 | max-width: 75vw; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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_segment { 10 | color: var(--u-color_text); 11 | } 12 | 13 | .fully-qualified-name .fully-qualified-name_segment.clickable:hover { 14 | color: var(--u-color_interactive_hovered); 15 | } 16 | 17 | .fully-qualified-name .fully-qualified-name_separator { 18 | color: var(--u-color_text_subdued); 19 | padding: 0 0.125ch; 20 | } 21 | -------------------------------------------------------------------------------- /src/css/code/hash.css: -------------------------------------------------------------------------------- 1 | .hash { 2 | display: inline-flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: 0; 6 | font-size: var(--font-size-small); 7 | color: var(--u-color_text_subdued); 8 | font-family: var(--font-monospace); 9 | } 10 | 11 | .hash .icon { 12 | /* Slight alignment hack for being shown next to text */ 13 | margin-bottom: 1px; 14 | font-size: var(--font-size-medium); 15 | color: var(--u-color_icon_subdued); 16 | } 17 | -------------------------------------------------------------------------------- /src/css/code/hashvatar.css: -------------------------------------------------------------------------------- 1 | .hashvatar { 2 | position: relative; 3 | display: inline-flex; 4 | width: 1.5rem; 5 | height: 1.5rem; 6 | border-radius: var(--border-radius-base); 7 | overflow: clip; 8 | } 9 | -------------------------------------------------------------------------------- /src/css/code/project-name-listing.css: -------------------------------------------------------------------------------- 1 | .project-name-listing { 2 | display: inline-flex; 3 | width: fit-content; 4 | flex-direction: row; 5 | align-items: center; 6 | gap: 0.5rem; 7 | color: var(--u-color_text); 8 | font-size: var(--font-size-medium); 9 | height: 1.25rem; 10 | line-height: 0; 11 | } 12 | 13 | .project-name-listing.project-name-listing-size_huge .hashvatar { 14 | width: 1.25rem; 15 | height: 1.25rem; 16 | } 17 | 18 | .project-name-listing.project-name-listing-size_large { 19 | font-size: var(--font-size-base); 20 | height: 1.5rem; 21 | gap: 0.625rem; 22 | } 23 | 24 | .project-name-listing.project-name-listing-size_large .hashvatar { 25 | width: 1.5rem; 26 | height: 1.5rem; 27 | } 28 | 29 | .project-name-listing.project-name-listing-size_huge { 30 | font-size: 1.125rem; 31 | gap: 0.75rem; 32 | } 33 | 34 | .project-name-listing.project-name-listing-size_huge .hashvatar { 35 | width: 2rem; 36 | height: 2rem; 37 | } 38 | 39 | .project-name-listing .hashvatar { 40 | transition: all 0.2s; 41 | } 42 | 43 | .project-name-listing a > .hashvatar:hover { 44 | transform: scale3d(1.1, 1.1, 1.1); 45 | } 46 | 47 | .project-name-listing 48 | a:has(.hashvatar:hover) 49 | + .project-name 50 | .project-name_slug { 51 | color: var(--u-color_interactive_hovered); 52 | } 53 | 54 | a.project-name-listing { 55 | padding-right: 0.375rem; 56 | /* border-radius is halved, because it is going to appear as dobbled 57 | * when applied via the box-shadow on hover */ 58 | border-radius: calc(var(--border-radius-base) / 2); 59 | } 60 | 61 | a.project-name-listing * { 62 | cursor: pointer; 63 | } 64 | 65 | a.project-name-listing:hover { 66 | text-decoration: none; 67 | background: var(--u-color_element_hovered); 68 | box-shadow: 0 0 0 0.25rem var(--u-color_element_hovered); 69 | } 70 | 71 | .project-name-listing.project-name-listing_subdued { 72 | & .project-name { 73 | --c-color_project-name_handle: var(--u-color_text_subdued); 74 | --c-color_project-name_slug: var(--u-color_text_subdued); 75 | } 76 | } 77 | 78 | .project-name-listing.project-name-listing_very-subdued { 79 | & .hashvatar { 80 | opacity: 0.75; 81 | } 82 | 83 | & .project-name { 84 | --c-color_project-name_handle: var(--u-color_text_subdued); 85 | --c-color_project-name_slug: var(--u-color_text_subdued); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/css/code/project-name.css: -------------------------------------------------------------------------------- 1 | .project-name { 2 | --c-color_project-name_handle: var(--u-color_text_subdued); 3 | --c-color_project-name_separator: var(--u-color_text_very-subdued); 4 | --c-color_project-name_slug: var(--u-color_text); 5 | 6 | font-weight: bold; 7 | display: inline-flex; 8 | flex-direction: row; 9 | gap: 0.25rem; 10 | } 11 | 12 | .project-name .project-name_handle { 13 | color: var(--c-color_project-name_handle); 14 | white-space: nowrap; 15 | } 16 | 17 | .project-name .project-name_separator { 18 | color: var(--c-color_project-name_separator); 19 | } 20 | 21 | .project-name .project-name_slug { 22 | color: var(--c-color_project-name_slug); 23 | white-space: nowrap; 24 | } 25 | 26 | .project-name a.project-name_handle:hover, 27 | .project-name a.project-name_slug:hover { 28 | color: var(--u-color_interactive_hovered); 29 | } 30 | -------------------------------------------------------------------------------- /src/css/code/version.css: -------------------------------------------------------------------------------- 1 | .version { 2 | display: inline-flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: 0; 6 | color: var(--u-color_text); 7 | font-family: var(--font-monospace); 8 | font-weight: bold; 9 | } 10 | -------------------------------------------------------------------------------- /src/css/code/workspace.css: -------------------------------------------------------------------------------- 1 | @import "./workspace-item.css"; 2 | @import "./workspace-minimap.css"; 3 | 4 | #workspace { 5 | display: flex; 6 | flex-direction: column; 7 | background: var(--color-gray-lighten-60); 8 | } 9 | 10 | #workspace #workspace-content { 11 | /*overflow-x: hidden;*/ 12 | display: flex; 13 | flex-direction: column; 14 | gap: 1rem; 15 | } 16 | 17 | #workspace .workspace-minimap { 18 | position: absolute; 19 | z-index: var(--layer-floating-controls); 20 | right: 1rem; 21 | margin-top: -0.5rem; 22 | max-height: 75%; 23 | overflow: auto; 24 | } 25 | 26 | @media only screen and (--u-viewport_max-lg) { 27 | #workspace .workspace-minimap { 28 | margin-top: -2.5rem; 29 | right: 0.5rem; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/css/ui.css: -------------------------------------------------------------------------------- 1 | @import "./ui/core.css"; 2 | @import "./ui/colors.css"; 3 | @import "./ui/fonts.css"; 4 | @import "./ui/base.css"; 5 | @import "./ui/animations.css"; 6 | @import "./ui/components.css"; 7 | @import "./ui/page-layout.css"; 8 | -------------------------------------------------------------------------------- /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 pulsate-size { 56 | 0% { 57 | transform: scale3D(1, 1, 1); 58 | } 59 | 50% { 60 | transform: scale3D(1.5, 1.5, 1.5); 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: var(--line-height-base); 42 | background: var(--u-color_background); 43 | color: var(--u-color_text); 44 | overflow: overlay; /* helps with transparent scrollbar tracks (technically deprecated property) */ 45 | } 46 | 47 | input, 48 | textarea { 49 | border: 0; 50 | border-radius: 0; 51 | font-family: var(--font-sans-serif); 52 | caret-color: var(--u-color_caret); 53 | } 54 | 55 | ::placeholder { 56 | color: var(--u-color_text_subdued); 57 | opacity: 1; 58 | } 59 | 60 | li::marker { 61 | color: var(--u-color_icon); 62 | } 63 | 64 | mark { 65 | color: var(--u-color_search-highlight_text); 66 | background: var(--u-color_search-highlight_background); 67 | } 68 | 69 | * { 70 | scrollbar-width: 0.5rem; 71 | scrollbar-color: var(--u-color_scrollbar) var(--u-color_scrollbar-track); 72 | } 73 | 74 | ::-webkit-scrollbar { 75 | height: 0.375rem; 76 | width: 0.5rem; 77 | } 78 | 79 | ::-webkit-scrollbar-track { 80 | background: var(--u-color_scrollbar-track); 81 | } 82 | 83 | ::-webkit-scrollbar-thumb { 84 | background-color: var(--u-color_scrollbar); 85 | border-radius: var(--border-radius-base); 86 | } 87 | 88 | a, 89 | a:visited { 90 | text-decoration: none; 91 | cursor: pointer; 92 | transition: all 0.2s; 93 | color: var(--u-color_interactive); 94 | } 95 | 96 | a * { 97 | cursor: pointer; 98 | } 99 | 100 | a:hover { 101 | color: var(--u-color_interactive_hovered); 102 | } 103 | 104 | a:active { 105 | color: var(--u-color_interactive_pressed); 106 | } 107 | -------------------------------------------------------------------------------- /src/css/ui/components.css: -------------------------------------------------------------------------------- 1 | @import "./components/icon.css"; 2 | @import "./components/by-at.css"; 3 | @import "./components/text.css"; 4 | @import "./components/button.css"; 5 | @import "./components/tooltip.css"; 6 | @import "./components/fold-toggle.css"; 7 | @import "./components/card.css"; 8 | @import "./components/toolbar.css"; 9 | @import "./components/app-header.css"; 10 | @import "./components/modal.css"; 11 | @import "./components/codebase-tree.css"; 12 | @import "./components/copy-field.css"; 13 | @import "./components/copy-on-click.css"; 14 | @import "./components/keyboard-shortcuts.css"; 15 | @import "./components/badges.css"; 16 | @import "./components/divider.css"; 17 | @import "./components/loading-placeholder.css"; 18 | @import "./components/navigation.css"; 19 | @import "./components/avatar.css"; 20 | @import "./components/avatar-stack.css"; 21 | @import "./components/profile-snippet.css"; 22 | @import "./components/sidebar.css"; 23 | @import "./components/placeholder.css"; 24 | @import "./components/status-indicator.css"; 25 | @import "./components/status-banner.css"; 26 | @import "./components/steps.css"; 27 | @import "./components/status-message.css"; 28 | @import "./components/action-menu.css"; 29 | @import "./components/nudge.css"; 30 | @import "./components/page-header.css"; 31 | @import "./components/tag.css"; 32 | @import "./components/kpi-tag.css"; 33 | @import "./components/error-card.css"; 34 | @import "./components/form.css"; 35 | @import "./components/anchored-overlay.css"; 36 | @import "./components/empty-state.css"; 37 | @import "./components/empty-state-card.css"; 38 | @import "./components/tab-list.css"; 39 | @import "./components/external-link-icon.css"; 40 | @import "./components/date-time.css"; 41 | -------------------------------------------------------------------------------- /src/css/ui/components/avatar-stack.css: -------------------------------------------------------------------------------- 1 | .avatar-stack { 2 | display: flex; 3 | flex-direction: row; 4 | gap: -0.5rem; 5 | transition: all 0.2s; 6 | } 7 | 8 | .avatar-stack:hover { 9 | gap: 0.25rem; 10 | } 11 | -------------------------------------------------------------------------------- /src/css/ui/components/avatar.css: -------------------------------------------------------------------------------- 1 | .avatar { 2 | --avatar_size: 1.5rem; 3 | --avatar_text_size: 0.625rem; 4 | --color_avatar: var(--u-color_element_emphasized); 5 | --color_avatar_blank-icon: var(--u-color_icon_subdued); 6 | --color_avatar_text: var(--u-color_text); 7 | 8 | width: var(--avatar_size); 9 | height: var(--avatar_size); 10 | border-radius: calc(var(--avatar_size) / 2); 11 | display: inline-flex; 12 | align-items: center; 13 | justify-content: center; 14 | background-color: transparent; /* Don't use a color when we don't yet know if there's an image loading */ 15 | background-size: cover; 16 | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2); 17 | } 18 | 19 | .avatar.small { 20 | --avatar_size: 1.125rem; 21 | --avatar_text_size: 0.675rem; 22 | } 23 | 24 | .avatar.medium { 25 | --avatar_size: 1.5rem; 26 | --avatar_text_size: 0.675rem; 27 | } 28 | 29 | .avatar.large { 30 | --avatar_size: 2.625rem; 31 | --avatar_text_size: 1rem; 32 | } 33 | 34 | .avatar.huge { 35 | --avatar_size: 4.5rem; 36 | --avatar_text_size: 1.5rem; 37 | } 38 | 39 | .avatar.avatar_text { 40 | font-size: var(--avatar_text_size); 41 | text-transform: uppercase; 42 | font-weight: bold; 43 | line-height: 1; 44 | } 45 | 46 | .avatar.avatar_blank { 47 | background-color: var(--color_avatar); 48 | } 49 | 50 | .avatar.avatar_blank-icon { 51 | background-color: var(--color_avatar); 52 | } 53 | 54 | .avatar.avatar_blank-icon .icon { 55 | background-color: var(--color_avatar); 56 | color: var(--color_avatar_blank-icon); 57 | } 58 | 59 | .avatar.small.avatar_blank-icon .icon { 60 | font-size: 0.5rem; 61 | } 62 | 63 | .avatar.medium.avatar_blank-icon .icon { 64 | font-size: 0.75rem; 65 | } 66 | 67 | .avatar.large.avatar_blank-icon .icon { 68 | font-size: 1.5rem; 69 | } 70 | 71 | .avatar.huge.avatar_blank-icon .icon { 72 | font-size: 2.25rem; 73 | } 74 | -------------------------------------------------------------------------------- /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-lighten-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/by-at.css: -------------------------------------------------------------------------------- 1 | .by-at { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 0.25rem; /* TODO: should be 0.5rem when there's an avatar instead of handle */ 5 | font-size: var(--font-size-small); 6 | color: var(--u-color_text_subdued); 7 | align-items: center; 8 | } 9 | 10 | .by-at .profile-snippet { 11 | --color_profile-snippet_text: var(--u-color_text_subdued); 12 | } 13 | 14 | .by-at .by-at_at { 15 | /* Total hack, not sure why its 1px off..*/ 16 | padding-top: 1px; 17 | } 18 | 19 | a.by-at, 20 | a.by-at:visited { 21 | color: var(--u-color_text_subdued); 22 | } 23 | 24 | a.by-at:hover .profile-snippet { 25 | --color_profile-snippet_text: var(--u-color_interactive); 26 | --color_profile-snippet_text_subdued: var(--u-color_interactive); 27 | } 28 | 29 | a.by-at:hover { 30 | color: var(--u-color_text_subdued); 31 | } 32 | -------------------------------------------------------------------------------- /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 | --c-card_padding: 1.5rem; 7 | 8 | padding: var(--c-card_padding); 9 | display: flex; 10 | flex-direction: column; 11 | align-items: flex-start; 12 | gap: 0.75rem; 13 | color: var(--color-card-text); 14 | border-radius: var(--border-radius-base); 15 | background: var(--color-card-bg); 16 | } 17 | 18 | .card.card_tight-padding { 19 | --c-card_padding: 0.375rem; 20 | } 21 | 22 | .card.contained { 23 | border: 1px solid var(--color-card-border); 24 | } 25 | 26 | .card:has(.card-title) { 27 | padding-top: 1rem; 28 | } 29 | 30 | .card .card-title { 31 | color: var(--color-card-title); 32 | text-transform: uppercase; 33 | font-size: var(--font-size-extra-small); 34 | font-weight: bold; 35 | display: flex; 36 | align-items: center; 37 | height: 2rem; 38 | } 39 | 40 | .card.contained-with-fade.contained-with-fade_surface-background { 41 | --color-card-on-surface-bg: var(--u-color_background); 42 | } 43 | 44 | .card.contained-with-fade.contained-with-fade_surface-background-subdued { 45 | --color-card-on-surface-bg: var(--u-color_background_subdued); 46 | } 47 | 48 | .card.contained-with-fade { 49 | padding: 4rem 0; 50 | background: 51 | linear-gradient(var(--color-card-bg), var(--color-card-on-surface-bg)) 52 | padding-box, 53 | linear-gradient( 54 | var(--color-card-border), 55 | var(--color-card-border), 56 | var(--color-card-on-surface-bg) 57 | ) 58 | border-box; 59 | border-radius: var(--border-radius-base); 60 | border: 1px solid transparent; 61 | } 62 | -------------------------------------------------------------------------------- /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 | line-height: 1; 17 | } 18 | 19 | .codebase-tree .error { 20 | padding-left: 0.5rem; 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | margin: 0.5rem 0; 25 | color: var(--u-color_critical_text); 26 | } 27 | 28 | .codebase-tree .error .icon { 29 | font-size: 1rem; 30 | margin-right: 0.25rem; 31 | color: var(--u-color_critical_icon); 32 | } 33 | 34 | .codebase-tree .loading { 35 | padding-left: 0.5rem; 36 | } 37 | 38 | .codebase-tree .loading .loading-placeholder { 39 | width: 8rem; 40 | } 41 | 42 | .codebase-tree .namespace-content .loading { 43 | padding-left: 0.875rem; 44 | } 45 | 46 | .codebase-tree .namespace-content .loading .loading-placeholder { 47 | width: 6rem; 48 | } 49 | 50 | .codebase-tree .namespace-tree .node:hover { 51 | background: var(--color-sidebar-focus-bg); 52 | text-decoration: none; 53 | } 54 | 55 | .codebase-tree .namespace-tree .node > .icon { 56 | font-size: 0.875rem; 57 | text-align: center; 58 | margin-right: 0.5rem; 59 | transition: transform 0.1s ease-out; 60 | flex-shrink: 0; 61 | } 62 | 63 | .codebase-tree .namespace-tree .node > .namespace-icon > .icon { 64 | font-size: 0.875rem; 65 | text-align: center; 66 | margin-right: 0.5rem; 67 | transition: transform 0.1s ease-out; 68 | flex-shrink: 0; 69 | color: var(--color-sidebar-subtle-fg); 70 | } 71 | 72 | .codebase-tree .namespace-tree .node:hover > .namespace-icon > .icon { 73 | color: var(--color-sidebar-focus-fg); 74 | } 75 | 76 | .codebase-tree .namespace-tree .node > label { 77 | color: var(--color-sidebar-fg); 78 | transition: all 0.2s; 79 | cursor: pointer; 80 | overflow: hidden; 81 | text-overflow: ellipsis; 82 | line-height: 1.875; 83 | } 84 | 85 | .codebase-tree .namespace-tree .node > .tooltip-trigger { 86 | margin-left: auto; 87 | opacity: 0; 88 | } 89 | 90 | .codebase-tree .namespace-tree .node:hover > .tooltip-trigger { 91 | opacity: 1; 92 | } 93 | 94 | .codebase-tree .namespace-tree .node:hover .tooltip-bubble { 95 | right: -2rem; 96 | min-width: calc(var(--c-width_sidebar) - 1.5rem); 97 | margin-top: 0.75rem; 98 | } 99 | 100 | .codebase-tree .namespace-tree .node:hover label { 101 | color: var(--color-sidebar-focus-fg); 102 | } 103 | 104 | .codebase-tree .namespace-tree .node.open label { 105 | font-weight: bold; 106 | } 107 | 108 | .codebase-tree .namespace-tree .namespace-content { 109 | margin-left: 1rem; 110 | } 111 | -------------------------------------------------------------------------------- /src/css/ui/components/copy-on-click.css: -------------------------------------------------------------------------------- 1 | .copy-on-click { 2 | position: relative; 3 | 4 | & .copy-on-click_success { 5 | opacity: 0; 6 | pointer-events: none; 7 | transition: all 0.5s; 8 | } 9 | } 10 | 11 | .copy-on-click.copy-success { 12 | & .copy-on-click_success { 13 | opacity: 1; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/css/ui/components/date-time.css: -------------------------------------------------------------------------------- 1 | .date-time { 2 | position: relative; 3 | } 4 | 5 | .tooltip-trigger .date-time:hover { 6 | text-decoration: underline; 7 | text-decoration-style: dotted; 8 | } 9 | -------------------------------------------------------------------------------- /src/css/ui/components/divider.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | border: 0; 3 | height: 2px; 4 | border-radius: 1px; 5 | width: 100%; 6 | margin: 0; 7 | } 8 | 9 | .divider.divider_size_small { 10 | height: 1px; 11 | } 12 | 13 | .divider.divider_on-surface_light { 14 | background: var(--u-color_divider); 15 | } 16 | 17 | .divider.divider_on-surface_dark { 18 | /* @color-todo */ 19 | background: var(--color-gray-darken-10); 20 | } 21 | 22 | .divider.divider_margin { 23 | margin: 1.5rem 0; 24 | } 25 | -------------------------------------------------------------------------------- /src/css/ui/components/empty-state-card.css: -------------------------------------------------------------------------------- 1 | .empty-state-card { 2 | display: flex; 3 | flex-direction: column; 4 | align-self: center; 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/css/ui/components/empty-state.css: -------------------------------------------------------------------------------- 1 | .empty-state { 2 | font-size: var(--font-size-medium); 3 | color: var(--u-color_text); 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | /* -- Grid ----------------------------------------------------------------- */ 9 | 10 | .empty-state .empty-state_grid { 11 | background: url(../empty-state_grid.svg); 12 | background-position: center 2rem; 13 | background-repeat: no-repeat; 14 | background-size: contain; 15 | padding: 0rem 4.625rem 5.5rem 4.625rem; 16 | display: flex; 17 | justify-content: center; 18 | width: 37.5rem; 19 | } 20 | 21 | /* -- Icon Cloud ----------------------------------------------------------- */ 22 | 23 | .empty-state:has(.empty-state_icon-cloud) { 24 | gap: 1.25rem; 25 | align-self: center; 26 | place-content: center; 27 | place-items: center; 28 | } 29 | 30 | .empty-state:has(.empty-state_icon-cloud) p { 31 | margin: 0; 32 | } 33 | 34 | .empty-state .empty-state_icon-cloud { 35 | position: relative; 36 | width: 317px; 37 | height: 174px; 38 | background: url("../empty-state_icon-cloud.svg"); 39 | } 40 | 41 | .empty-state .empty-state_icon-cloud_center-piece { 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | background: var(--u-color_element_emphasized); 46 | position: absolute; 47 | left: 50%; 48 | top: 50%; 49 | transform: translate(-50%, -25%); 50 | width: 4rem; 51 | height: 4rem; 52 | border-radius: 2rem; 53 | font-size: 2rem; 54 | } 55 | 56 | .empty-state .empty-state_icon-cloud_center-piece_icon { 57 | display: flex; 58 | justify-content: center; 59 | align-items: center; 60 | background: var(--u-color_element_emphasized); 61 | position: absolute; 62 | box-shadow: 0 0 0 0.25rem var(--u-color_element_emphasized); 63 | left: 50%; 64 | top: 50%; 65 | transform: translate(-50%, -25%); 66 | width: 4rem; 67 | height: 4rem; 68 | border-radius: 2rem; 69 | font-size: 2rem; 70 | line-height: 1; 71 | } 72 | 73 | .empty-state .empty-state_icon-cloud_center-piece_icon .icon { 74 | font-size: 2rem; 75 | } 76 | 77 | /* -- Search --------------------------------------------------------------- */ 78 | 79 | .empty-state:has(.empty-state_search) { 80 | gap: 1rem; 81 | place-content: center; 82 | align-items: center; 83 | align-self: center; 84 | } 85 | 86 | .empty-state:has(.empty-state_search) p { 87 | margin-bottom: 0; 88 | } 89 | 90 | .empty-state .empty-state_search { 91 | width: 224px; 92 | height: 68px; 93 | margin-bottom: 1rem; 94 | } 95 | 96 | .empty-state.on-light .empty-state_search { 97 | background: url("../empty-state_search_on-light.svg"); 98 | } 99 | 100 | .empty-state.on-dark .empty-state_search { 101 | background: url("../empty-state_search_on-dark.svg"); 102 | } 103 | -------------------------------------------------------------------------------- /src/css/ui/components/error-card.css: -------------------------------------------------------------------------------- 1 | .error-card { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1.5rem; 5 | justify-content: center; 6 | text-align: center; 7 | max-width: 24rem; 8 | align-self: center; 9 | } 10 | 11 | .error-card .error-card_details { 12 | display: flex; 13 | flex-direction: column; 14 | gap: 1rem; 15 | } 16 | 17 | .error-card .emoji { 18 | font-size: 2.625rem; 19 | } 20 | 21 | .error-card p { 22 | margin-bottom: 0; 23 | font-size: var(--font-size-medium); 24 | } 25 | 26 | .error-card .button { 27 | align-self: center; 28 | } 29 | -------------------------------------------------------------------------------- /src/css/ui/components/external-link-icon.css: -------------------------------------------------------------------------------- 1 | .external-link-icon { 2 | width: 1.5rem; 3 | height: 1.5rem; 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | justify-content: center; 8 | border-radius: 0.75rem; 9 | } 10 | 11 | .external-link-icon:hover { 12 | background: var(--u-color_element_emphasized); 13 | } 14 | 15 | .external-link-icon:hover .icon { 16 | color: var(--u-color_icon_hovered); 17 | } 18 | -------------------------------------------------------------------------------- /src/css/ui/components/fold-toggle.css: -------------------------------------------------------------------------------- 1 | .fold-toggle { 2 | --color-fold-toggle-icon: var(--u-color_icon); 3 | --color-fold-toggle-hover-icon: var(--u-color_interactive_hovered); 4 | 5 | display: inline-flex; 6 | align-items: center; 7 | justify-content: center; 8 | flex-direction: row; 9 | width: 1.25rem; 10 | height: 1.25rem; 11 | margin-right: 0.25rem; 12 | line-height: 1; 13 | border-radius: var(--border-radius-base); 14 | transition: all 0.2s; 15 | cursor: pointer; 16 | } 17 | 18 | .fold-toggle.disabled { 19 | cursor: default; 20 | opacity: 0.5; 21 | } 22 | 23 | /* ▶ */ 24 | .fold-toggle .icon { 25 | color: var(--color-fold-toggle-icon); 26 | transition: all 0.2s ease-out; 27 | } 28 | 29 | /* rotate ▶ to ▼ */ 30 | .fold-toggle.folded-open { 31 | transform: rotate(90deg); 32 | } 33 | 34 | .fold-toggle:not(.disabled):hover .icon { 35 | color: var(--color-fold-toggle-hover-icon); 36 | transform: scale(1.25); 37 | } 38 | -------------------------------------------------------------------------------- /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-separator: var(--u-color_text_subdued); 3 | 4 | display: flex; 5 | flex-direction: row; 6 | justify-self: flex-end; 7 | margin-left: auto; 8 | } 9 | 10 | .keyboard-shortcut { 11 | --color-keyboard-shortcut-key-text: var(--u-color_text-on-element-emphasized); 12 | --color-keyboard-shortcut-key-bg: var(--u-color_element_emphasized); 13 | --color-keyboard-shortcut-key-border: var(--u-color_border); 14 | --color-keyboard-shortcut-key-shadow: var(--u-color_border); 15 | --color-keyboard-shortcut-then: var(--u-color_text_subdued); 16 | 17 | display: flex; 18 | flex-direction: row; 19 | gap: 0.25rem; 20 | } 21 | 22 | .keyboard-shortcut .key { 23 | height: 1.5rem; 24 | min-width: 1.5rem; 25 | font-size: 0.75rem; 26 | padding: 0.375rem; 27 | border-radius: var(--border-radius-base); 28 | text-align: center; 29 | display: inline-flex; 30 | justify-content: center; 31 | align-content: center; 32 | align-items: center; 33 | color: var(--color-keyboard-shortcut-key-text); 34 | background: var(--color-keyboard-shortcut-key-bg); 35 | border: 1px solid var(--color-keyboard-shortcut-key-border); 36 | box-shadow: 0 1px 0 var(--color-keyboard-shortcut-key-shadow); 37 | transition: all 0.2s; 38 | font-family: var(--font-sans-serif); 39 | } 40 | 41 | .keyboard-shortcuts .separator { 42 | display: inline-flex; 43 | height: 1.5rem; 44 | font-size: 0.875rem; 45 | color: var(--color-keyboard-shortcut-separator); 46 | align-items: center; 47 | margin: 0 0.4rem; 48 | line-height: 1; 49 | } 50 | 51 | .keyboard-shortcut .then { 52 | display: inline-flex; 53 | height: 1.5rem; 54 | font-size: 0.625rem; 55 | line-height: 1; 56 | margin: 0 0.35rem; 57 | color: var(--color-keyboard-shortcut-then); 58 | align-items: center; 59 | } 60 | -------------------------------------------------------------------------------- /src/css/ui/components/kpi-tag.css: -------------------------------------------------------------------------------- 1 | .kpi-tag { 2 | --c-color_kpi-tag_kpi: var(--u-color_text); 3 | --c-color_kpi-tag_icon: var(--u-color_icon); 4 | --c-color_kpi-tag_label: var(--u-color_text-on-element-emphasized); 5 | --c-color_kpi-tag_bg: var(--u-color_element_emphasized); 6 | --c-color_kpi-tag_bg_hovered: var(--u-color_element_emphasized_hovered); 7 | 8 | display: inline-flex; 9 | flex-direction: row; 10 | gap: 0.5rem; 11 | height: 2rem; 12 | place-items: center; 13 | place-content: center; 14 | padding: 0 1rem; 15 | line-height: 1; 16 | border-radius: 1rem; 17 | background: var(--c-color_kpi-tag_bg); 18 | transition: background 0.2s; 19 | } 20 | 21 | .kpi-tag.kpi-tag_interactive:hover { 22 | background: var(--c-color_kpi-tag_bg_hovered); 23 | } 24 | 25 | .kpi-tag .kpi-tag_inner { 26 | display: inline-flex; 27 | flex-direction: row; 28 | gap: 0.5rem; 29 | place-items: center; 30 | place-content: center; 31 | } 32 | 33 | .kpi-tag .kpi-tag_kpi { 34 | font-weight: bold; 35 | font-family: var(--font-monospace); 36 | font-size: var(--font-size-small); 37 | color: var(--c-color_kpi-tag_kpi); 38 | margin-top: 2px; 39 | } 40 | 41 | .kpi-tag .icon { 42 | font-size: var(--font-size-base); 43 | color: var(--c-color_kpi-tag_icon); 44 | } 45 | 46 | .kpi-tag .kpi-tag_label { 47 | font-size: var(--font-size-small); 48 | color: var(--c-color_kpi-tag_label); 49 | } 50 | 51 | .kpi-tag_icon-then-label { 52 | display: inline-flex; 53 | flex-direction: row; 54 | gap: 0.25rem; 55 | height: 2rem; 56 | line-height: 1; 57 | place-items: center; 58 | place-content: center; 59 | } 60 | 61 | .kpi-tag .tooltip:is(.left-of, .right-of).arrow-middle .tooltip-bubble { 62 | transform: none; 63 | } 64 | 65 | .kpi-tag .tooltip.left-of .tooltip-bubble { 66 | right: 1.5rem; 67 | } 68 | 69 | .kpi-tag .tooltip.right-of .tooltip-bubble { 70 | left: 1.5rem; 71 | } 72 | -------------------------------------------------------------------------------- /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/navigation.css: -------------------------------------------------------------------------------- 1 | .navigation { 2 | --u-color_c_navigation-item: var(--color-transparent); 3 | --u-color_c_navigation-item_hovered: var(--color-gray-base); 4 | --u-color_c_text-on-navigation-item: var(--u-color_text); 5 | --u-color_c_icon-on-navigation-item: var(--u-color_icon); 6 | --u-color_c_text-on-navigation-item_hovered: var(--color-gray-lighten-60); 7 | --u-color_c_navigation-item: var(--color-transparent); 8 | --u-color_c_navigation-item_selected: var(--color-gray-darken-20); 9 | --u-color_c_text-on-navigation-item_selected: var(--color-gray-lighten-60); 10 | 11 | display: flex; 12 | } 13 | 14 | .navigation .nav-item { 15 | position: relative; 16 | background: var(--u-color_c_navigation-item); 17 | transition: background 0.2s; 18 | cursor: pointer; 19 | } 20 | 21 | .navigation .nav-item_content .nav-item_short-label { 22 | display: none; 23 | } 24 | 25 | .navigation .nav-item_content, 26 | .navigation .nav-item_inner-content { 27 | display: flex; 28 | flex-direction: row; 29 | height: 100%; 30 | gap: 0.5rem; 31 | align-items: center; 32 | justify-content: center; 33 | line-height: 1; 34 | } 35 | 36 | .navigation .nav-item_click-target { 37 | height: 100%; 38 | display: flex; 39 | flex-direction: row; 40 | align-items: center; 41 | justify-content: center; 42 | color: var(--u-color_c_text-on-navigation-item); 43 | } 44 | 45 | .navigation .nav-item_click-target .icon { 46 | color: var(--u-color_c_icon-on-navigation-item); 47 | } 48 | 49 | .navigation .nav-item.selected { 50 | color: var(--u-color_c_text-on-navigation-item_selected); 51 | background: var(--u-color_c_navigation-item_selected); 52 | } 53 | 54 | .navigation .nav-item:hover { 55 | color: var(--u-color_c_text-on-navigation-item_hovered); 56 | background: var(--u-color_c_navigation-item_hovered); 57 | } 58 | 59 | .navigation .nav-item_secondary { 60 | line-height: var(--line-height-base); 61 | } 62 | 63 | @media only screen and (--u-viewport_max-sm) { 64 | .navigation .nav-item_content .nav-item_short-label { 65 | display: flex; 66 | } 67 | 68 | .navigation .nav-item_content .nav-item_full-label { 69 | display: none; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/css/ui/components/nudge.css: -------------------------------------------------------------------------------- 1 | .nudge { 2 | position: relative; 3 | display: inline-flex; 4 | --c-color_nudge: var(--u-color_info_icon); 5 | } 6 | 7 | .nudge.nudge_emphasized { 8 | --c-color_nudge: var(--u-color_info_icon_emphasized); 9 | } 10 | 11 | .nudge .nudge_circle { 12 | background: var(--c-color_nudge); 13 | width: 0.5rem; 14 | height: 0.5rem; 15 | border-radius: 0.25rem; 16 | } 17 | 18 | .nudge.with-number .nudge_circle { 19 | min-width: 1rem; 20 | width: auto; 21 | padding: 0 0.25rem; 22 | height: 1rem; 23 | border-radius: 0.5rem; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | font-size: var(--font-size-extra-small); 28 | color: var(--u-color_text); 29 | font-weight: bold; 30 | font-family: var(--font-monospace); 31 | line-height: 1; 32 | } 33 | 34 | .nudge.pulsate .nudge_circle { 35 | animation-name: pulsate-size; 36 | animation-duration: 1500ms; 37 | animation-iteration-count: infinite; 38 | animation-timing-function: var(--anim-elastic); 39 | } 40 | -------------------------------------------------------------------------------- /src/css/ui/components/placeholder.css: -------------------------------------------------------------------------------- 1 | .placeholder.placeholder_intensity_subdued { 2 | opacity: 0.3; 3 | } 4 | 5 | .placeholder.placeholder_intensity_normal { 6 | opacity: 0.6; 7 | } 8 | 9 | .placeholder.placeholder_intensity_emphasized { 10 | opacity: 1; 11 | } 12 | 13 | /* -- Placeholder.Text ------------------------------------------------ */ 14 | 15 | .placeholder.placeholder_text { 16 | --c-color_placeholder_text: var(--u-color_text_very-subdued); 17 | --c-size_placeholder_text_tiny: 0.375rem; 18 | --c-size_placeholder_text_small: 0.5rem; 19 | --c-size_placeholder_text_medium: 0.5rem; 20 | --c-size_placeholder_text_large: 1rem; 21 | --c-size_placeholder_text_huge: 0.5rem; 22 | 23 | --c-length_placeholder_text_tiny: 2rem; 24 | --c-length_placeholder_text_small: 8rem; 25 | --c-length_placeholder_text_medium: 10rem; 26 | --c-length_placeholder_text_large: 16rem; 27 | --c-length_placeholder_text_huge: 20rem; 28 | 29 | --c-size_placeholder_text: var(--c-size_placeholder_text_medium); 30 | --c-length_placeholder_text: var(--c-length_placeholder_text_medium); 31 | } 32 | 33 | .placeholder.placeholder_text .placeholder_shape { 34 | border-radius: calc(var(--c-size_placeholder_text) / 2); 35 | height: var(--c-size_placeholder_text); 36 | width: var(--c-length_placeholder_text); 37 | background: var(--c-color_placeholder_text); 38 | } 39 | 40 | .placeholder.placeholder_text.placeholder_size_tiny { 41 | --c-size_placeholder_text: var(--c-size_placeholder_text_tiny); 42 | } 43 | .placeholder.placeholder_text.placeholder_size_small { 44 | --c-size_placeholder_text: var(--c-size_placeholder_text_small); 45 | } 46 | .placeholder.placeholder_text.placeholder_size_medium { 47 | --c-size_placeholder_text: var(--c-size_placeholder_text_medium); 48 | } 49 | .placeholder.placeholder_text.placeholder_size_large { 50 | --c-size_placeholder_text: var(--c-size_placeholder_text_large); 51 | } 52 | .placeholder.placeholder_text.placeholder_size_huge { 53 | --c-size_placeholder_text: var(--c-size_placeholder_text_huge); 54 | } 55 | 56 | .placeholder.placeholder_text.placeholder_length_tiny { 57 | --c-length_placeholder_text: var(--c-length_placeholder_text_tiny); 58 | } 59 | .placeholder.placeholder_text.placeholder_length_small { 60 | --c-length_placeholder_text: var(--c-length_placeholder_text_small); 61 | } 62 | .placeholder.placeholder_text.placeholder_length_medium { 63 | --c-length_placeholder_text: var(--c-length_placeholder_text_medium); 64 | } 65 | .placeholder.placeholder_text.placeholder_length_large { 66 | --c-length_placeholder_text: var(--c-length_placeholder_text_large); 67 | } 68 | .placeholder.placeholder_text.placeholder_length_huge { 69 | --c-length_placeholder_text: var(--c-length_placeholder_text_huge); 70 | } 71 | -------------------------------------------------------------------------------- /src/css/ui/components/profile-snippet.css: -------------------------------------------------------------------------------- 1 | .profile-snippet.profile-snippet_size_small { 2 | --c-size_profile-snippet_name: var(--font-size-small); 3 | font-weight: bold; 4 | } 5 | 6 | .profile-snippet.profile-snippet_size_medium { 7 | --c-size_profile-snippet_name: var(--font-size-medium); 8 | --c-size_profile-snippet_handle: var(--font-size-small); 9 | } 10 | 11 | .profile-snippet.profile-snippet_size_large { 12 | --c-size_profile-snippet_name: var(--font-size-medium); 13 | --c-size_profile-snippet_handle: var(--font-size-small); 14 | } 15 | 16 | .profile-snippet.profile-snippet_size_huge { 17 | --c-size_profile-snippet_name: var(--font-size-large); 18 | --c-size_profile-snippet_handle: var(--font-size-medium); 19 | } 20 | 21 | .profile-snippet { 22 | --color_profile-snippet_text: var(--u-color_text); 23 | --color_profile-snippet_text_subdued: var(--u-color_text_subdued); 24 | 25 | display: flex; 26 | flex-direction: row; 27 | align-items: center; 28 | gap: 0.5rem; 29 | transition: background 0.2s; 30 | } 31 | 32 | .profile-snippet .profile-snippet_text { 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | gap: 0.5rem; 37 | } 38 | 39 | .profile-snippet.profile-snippet_size_small { 40 | gap: 0.25rem; 41 | } 42 | 43 | .profile-snippet.profile-snippet_size_large, 44 | .profile-snippet.profile-snippet_size_huge { 45 | gap: 0.75rem; 46 | } 47 | 48 | .profile-snippet.profile-snippet_size_large .profile-snippet_text, 49 | .profile-snippet.profile-snippet_size_huge .profile-snippet_text { 50 | display: flex; 51 | flex-direction: column; 52 | align-items: flex-start; 53 | gap: 0; 54 | } 55 | .profile-snippet.profile-snippet_size_large.profile-snippet_loading 56 | .profile-snippet_text, 57 | .profile-snippet.profile-snippet_size_huge.profile-snippet_loading 58 | .profile-snippet_text { 59 | display: flex; 60 | gap: 0.75rem; 61 | } 62 | 63 | .profile-snippet .profile-snippet_handle { 64 | font-size: var(--c-size_profile-snippet_handle); 65 | color: var(--color_profile-snippet_text_subdued); 66 | padding-top: 1px; 67 | } 68 | 69 | .profile-snippet .profile-snippet_name, 70 | /* If there's no name, treat the handle the same as if it was the name */ 71 | .profile-snippet.profile-snippet_handle-only .profile-snippet_handle { 72 | font-size: var(--c-size_profile-snippet_name); 73 | font-weight: bold; 74 | color: var(--color_profile-snippet_text); 75 | /* Adjust to better align with avatar */ 76 | padding-top: 1px; 77 | text-overflow: ellipsis; 78 | overflow: hidden; 79 | white-space: nowrap; 80 | } 81 | -------------------------------------------------------------------------------- /src/css/ui/components/status-banner.css: -------------------------------------------------------------------------------- 1 | .status-banner { 2 | --c-color_status-banner_good: var(--u-color_positive_element_subdued); 3 | --c-color_status-banner_text-on-good: var( 4 | --u-color_positive_text-on-element_subdued 5 | ); 6 | 7 | --c-color_status-banner_bad: var(--u-color_critical_element_subdued); 8 | --c-color_status-banner_text-on-bad: var( 9 | --u-color_critical_text-on-element_subdued 10 | ); 11 | 12 | --c-color_status-banner_info: var(--u-color_info_element_subdued); 13 | --c-color_status-banner_text-on-info: var( 14 | --u-color_info_text-on-element_subdued 15 | ); 16 | 17 | --c-color_status-banner_working: var(--u-color_working_element_subdued); 18 | --c-color_status-banner_text-on-working: var( 19 | --u-color_working_text-on-element_subdued 20 | ); 21 | 22 | display: flex; 23 | flex-direction: row; 24 | align-items: center; 25 | gap: 0.5rem; 26 | padding: 0.375rem; 27 | padding-right: 0.75rem; 28 | color: var(--u-color_text); 29 | font-size: var(--font-size-medium); 30 | border-radius: 1rem; 31 | } 32 | .status-banner .status-indicator { 33 | align-self: flex-start; 34 | } 35 | 36 | .status-banner.status-banner_good { 37 | color: var(--c-color_status-banner_text-on-good); 38 | background: var(--c-color_status-banner_good); 39 | } 40 | 41 | .status-banner.status-banner_bad { 42 | color: var(--c-color_status-banner_text-on-bad); 43 | background: var(--c-color_status-banner_bad); 44 | } 45 | 46 | .status-banner.status-banner_info { 47 | color: var(--c-color_status-banner_text-on-info); 48 | background: var(--c-color_status-banner_info); 49 | } 50 | 51 | .status-banner.status-banner_working { 52 | color: var(--c-color_status-banner_text-on-working); 53 | background: var(--c-color_status-banner_working); 54 | } 55 | 56 | .status-banner .status-banner_content p:last-child { 57 | /* @todo: margin-trim */ 58 | margin-bottom: 0; 59 | } 60 | -------------------------------------------------------------------------------- /src/css/ui/components/status-indicator.css: -------------------------------------------------------------------------------- 1 | .status-indicator { 2 | --c-color_status-indicator_good: var(--u-color_positive_element); 3 | --c-color_status-indicator_icon-on-good: var( 4 | --u-color_positive_icon-on-element 5 | ); 6 | --c-color_status-indicator_bad: var(--u-color_critical_element); 7 | --c-color_status-indicator_icon-on-bad: var( 8 | --u-color_critical_icon-on-element 9 | ); 10 | --c-color_status-indicator_info: var(--u-color_info_element_emphasized); 11 | --c-color_status-indicator_icon-on-info: var( 12 | --u-color_info_icon-on-element_emphasized 13 | ); 14 | --c-color_status-indicator_working: var(--u-color_working_element); 15 | --c-color_status-indicator_icon-on-working: var( 16 | --u-color_working_icon-on-element 17 | ); 18 | 19 | --c-size_status-indicator: 1.25rem; 20 | --c-size_status-indicator_icon: 0.875rem; 21 | 22 | width: var(--c-size_status-indicator); 23 | height: var(--c-size_status-indicator); 24 | border-radius: calc(var(--c-size_status-indicator) / 2); 25 | display: inline-flex; 26 | align-items: center; 27 | justify-content: center; 28 | flex-shrink: 0; 29 | } 30 | 31 | .status-indicator.status-indicator_regular { 32 | --c-size_status-indicator: 1.25rem; 33 | --c-size_status-indicator_icon: 0.875rem; 34 | } 35 | 36 | .status-indicator.status-indicator_large { 37 | --c-size_status-indicator: 4rem; 38 | --c-size_status-indicator_icon: 2rem; 39 | } 40 | 41 | .status-indicator .icon { 42 | line-height: 1; 43 | font-size: var(--c-size_status-indicator_icon); 44 | } 45 | 46 | .status-indicator.status-indicator_good { 47 | background: var(--c-color_status-indicator_good); 48 | } 49 | 50 | .status-indicator.status-indicator_good .icon { 51 | color: var(--c-color_status-indicator_icon-on-good); 52 | } 53 | 54 | .status-indicator.status-indicator_bad { 55 | background: var(--c-color_status-indicator_bad); 56 | } 57 | 58 | .status-indicator.status-indicator_bad .icon { 59 | color: var(--c-color_status-indicator_icon-on-bad); 60 | } 61 | 62 | .status-indicator.status-indicator_info { 63 | background: var(--c-color_status-indicator_info); 64 | } 65 | .status-indicator.status-indicator_info .icon { 66 | color: var(--c-color_status-indicator_icon-on-info); 67 | } 68 | 69 | .status-indicator.status-indicator_working { 70 | background: var(--c-color_status-indicator_working); 71 | } 72 | .status-indicator.status-indicator_working .icon { 73 | color: var(--c-color_status-indicator_icon-on-working); 74 | animation-name: pulsate-size; 75 | animation-duration: 1500ms; 76 | animation-iteration-count: infinite; 77 | animation-timing-function: var(--anim-elastic); 78 | } 79 | -------------------------------------------------------------------------------- /src/css/ui/components/status-message.css: -------------------------------------------------------------------------------- 1 | .status-message { 2 | font-size: var(--font-size-base); 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | text-align: center; 7 | gap: 1rem; 8 | } 9 | 10 | .status-message .status-message_title { 11 | font-size: 1.125rem; 12 | margin-bottom: 0.5rem; 13 | } 14 | 15 | .status-message .button { 16 | margin-top: 1.5rem; 17 | } 18 | -------------------------------------------------------------------------------- /src/css/ui/components/steps.css: -------------------------------------------------------------------------------- 1 | .steps { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1.5rem; 5 | } 6 | 7 | .step { 8 | --c-size_step-number: 2rem; 9 | display: flex; 10 | flex-direction: row; 11 | gap: 1rem; 12 | } 13 | 14 | .step .step-number { 15 | display: flex; 16 | flex-shrink: 0; 17 | align-items: center; 18 | justify-content: center; 19 | font-weight: bold; 20 | width: var(--c-size_step-number); 21 | height: var(--c-size_step-number); 22 | border-radius: calc(var(--c-size_step-number) / 2); 23 | font-size: var(--font-size-base); 24 | background: var(--u-color_working_element); 25 | color: var(--u-color_working_icon-on-element); 26 | } 27 | 28 | .step .step-details { 29 | display: flex; 30 | flex-direction: column; 31 | gap: 0.5rem; 32 | padding: 0.25rem 0; 33 | } 34 | 35 | .step .step-details .step-title { 36 | font-size: var(--font-size-base); 37 | } 38 | 39 | .step .step-details .step-content { 40 | font-size: var(--font-size-medium); 41 | } 42 | -------------------------------------------------------------------------------- /src/css/ui/components/tab-list.css: -------------------------------------------------------------------------------- 1 | .tab-list { 2 | --c-color_tabs_text: var(--u-color_text_subdued); 3 | --c-color_tabs_border: var(--u-color_border_subdued); 4 | 5 | --c-color_tabs_text_selected: var(--u-color_text_emphasized); 6 | --c-color_tabs_border_selected: var(--u-color_border_emphasized); 7 | 8 | --c-color_tabs_text_hovered: var(--u-color_interactive); 9 | --c-color_tabs_border_hovered: var(--u-color_border_hovered); 10 | height: 2rem; 11 | display: flex; 12 | flex-direction: row; 13 | gap: 0.75rem; 14 | align-items: center; 15 | box-shadow: inset 0 -2px 0 -1px var(--c-color_tabs_border); 16 | width: 100%; 17 | } 18 | 19 | .tab-list .tab { 20 | height: 2rem; 21 | padding: 0 0.75rem; 22 | line-height: 1; 23 | font-size: var(--font-size-medium); 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | font-weight: bold; 28 | color: var(--c-color_tabs_text); 29 | -webkit-user-select: none; 30 | user-select: none; 31 | } 32 | 33 | .tab-list .tab.tab_selected { 34 | color: var(--c-color_tabs_text_selected); 35 | box-shadow: inset 0 -2px 0 -1px var(--c-color_tabs_border_selected); 36 | } 37 | 38 | .tab-list .tab:not(.tab_selected):hover { 39 | color: var(--c-color_tabs_text_hovered); 40 | box-shadow: inset 0 -2px 0 -1px var(--c-color_tabs_border_hovered); 41 | } 42 | -------------------------------------------------------------------------------- /src/css/ui/components/tag.css: -------------------------------------------------------------------------------- 1 | .tag { 2 | --c-color_tag_bg: var(--u-color_element_emphasized); 3 | --c-color_tag_bg_hovered: var(--u-color_element_emphasized_hovered); 4 | --c-color_tag_text: var(--u-color_text); 5 | --c-color_tag_text_subdued: var(--u-color_text_subdued); 6 | --c-color_tag_icon: var(--u-color_icon); 7 | --c-color_tag_icon_hovered: var(--u-color_icon_hovered); 8 | --c-color_tag_icon_subdued: var(--u-color_icon_subdued); 9 | 10 | background: var(--c-color_tag_bg); 11 | color: var(--c-color_tag_text); 12 | display: inline-flex; 13 | width: fit-content; 14 | align-items: center; 15 | flex-direction: row; 16 | line-height: 1; 17 | font-size: var(--font-size-small); 18 | } 19 | 20 | a.tag { 21 | color: var(--c-color_tag_text); 22 | } 23 | a.tag:hover { 24 | background: var(--c-color_tag_bg_hovered); 25 | } 26 | 27 | .tag.tag_size_medium { 28 | padding: 0 0.5rem; 29 | gap: 0.25rem; 30 | height: 1.25rem; 31 | border-radius: calc(1.25rem / 2); 32 | } 33 | 34 | .tag.tag_size_large { 35 | padding: 0 0.625rem; 36 | gap: 0.25rem; 37 | height: 1.5rem; 38 | border-radius: calc(1.5rem / 2); 39 | } 40 | 41 | .tag.tag_size_extra-large { 42 | padding: 0 0.75rem; 43 | gap: 0.5rem; 44 | height: 2rem; 45 | border-radius: calc(2rem / 2); 46 | font-size: var(--font-size-medium); 47 | } 48 | 49 | .tag.tag_size_extra-large .icon { 50 | font-size: var(--font-size-medium); 51 | } 52 | 53 | .tag .tag_text { 54 | display: inline-flex; 55 | flex-direction: row; 56 | gap: 0; 57 | } 58 | .tag .tag_text .tag_left-text, 59 | .tag .tag_text .tag_right-text { 60 | color: var(--c-color_tag_text_subdued); 61 | } 62 | 63 | .tag .tag_icon, 64 | .tag .tag_dismiss { 65 | display: flex; 66 | place-content: center; 67 | } 68 | 69 | .tag .tag_icon .icon { 70 | color: var(--c-color_tag_icon_subdued); 71 | font-size: var(--font-size-small); 72 | } 73 | 74 | .tag .tag_dismiss .icon { 75 | color: var(--c-color_tag_icon); 76 | font-size: var(--font-size-extra-small); 77 | } 78 | 79 | .tag .tag_dimiss:hover .icon { 80 | color: var(--c-color_tag_icon_hovered); 81 | } 82 | 83 | /* -- Collection ----------------------------------------------------------- */ 84 | 85 | .tags { 86 | display: flex; 87 | flex-direction: row; 88 | flex-wrap: wrap; 89 | gap: 0.25rem; 90 | } 91 | -------------------------------------------------------------------------------- /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: 3rem; 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/empty-state_search_on-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/css/ui/empty-state_search_on-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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("./fonts/Inter/Inter-roman.var.woff2") format("woff2"); 8 | } 9 | 10 | @font-face { 11 | font-family: "Inter var"; 12 | font-weight: 100 900; 13 | font-display: block; 14 | font-style: italic; 15 | font-named-instance: "Italic"; 16 | src: url("./fonts/Inter/Inter-italic.var.woff2") format("woff2"); 17 | } 18 | 19 | @font-face { 20 | font-family: "Fira Code var"; 21 | font-weight: 100 900; 22 | font-display: block; 23 | font-style: normal; 24 | font-named-instance: "Regular"; 25 | src: url("./fonts/FiraCode/FiraCode-VF.woff2") format("woff2-variations"); 26 | } 27 | 28 | @font-face { 29 | font-family: "Fira Code"; 30 | src: url("./fonts/FiraCode/FiraCode-Light.woff2") format("woff2"); 31 | font-weight: 300; 32 | font-style: normal; 33 | } 34 | 35 | @font-face { 36 | font-family: "Fira Code"; 37 | src: url("./fonts/FiraCode/FiraCode-Regular.woff2") format("woff2"); 38 | font-weight: 400; 39 | font-style: normal; 40 | } 41 | 42 | @font-face { 43 | font-family: "Fira Code"; 44 | src: url("./fonts/FiraCode/FiraCode-Medium.woff2") format("woff2"); 45 | font-weight: 500; 46 | font-style: normal; 47 | } 48 | 49 | @font-face { 50 | font-family: "Fira Code"; 51 | src: url("./fonts/FiraCode/FiraCode-SemiBold.woff2") format("woff2"); 52 | font-weight: 600; 53 | font-style: normal; 54 | } 55 | 56 | @font-face { 57 | font-family: "Fira Code"; 58 | src: url("./fonts/FiraCode/FiraCode-Bold.woff2") format("woff2"); 59 | font-weight: 700; 60 | font-style: normal; 61 | } 62 | -------------------------------------------------------------------------------- /src/css/ui/fonts/FiraCode/FiraCode-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/FiraCode/FiraCode-Bold.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/FiraCode/FiraCode-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/FiraCode/FiraCode-Light.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/FiraCode/FiraCode-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/FiraCode/FiraCode-Medium.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/FiraCode/FiraCode-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/FiraCode/FiraCode-Regular.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/FiraCode/FiraCode-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/FiraCode/FiraCode-SemiBold.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/FiraCode/FiraCode-VF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/FiraCode/FiraCode-VF.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/Inter/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/Inter/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/Inter/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/Inter/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /src/css/ui/fonts/Inter/Inter.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/src/css/ui/fonts/Inter/Inter.var.woff2 -------------------------------------------------------------------------------- /src/css/ui/viewport.css: -------------------------------------------------------------------------------- 1 | /* For this to work across all the css files consistently and without race conditions, 2 | * this needs be imported via the `importFrom` option in the webpack config 3 | */ 4 | 5 | /* Can't refer to variables unfortunately, so they are duplicated between here and ui.css. 6 | * also @custom-media must be outside :root for some reason.. */ 7 | @custom-media --u-viewport_max-sm (width <= 544px); /* --u-viewport_sm */ 8 | @custom-media --u-viewport_min-sm (width > 544px); /* --u-viewport_sm */ 9 | @custom-media --u-viewport_max-md (width <= 768px); /* --u-viewport_md */ 10 | @custom-media --u-viewport_min-md (width > 768px); /* --u-viewport_md */ 11 | @custom-media --u-viewport_max-lg (width <= 1012px); /* --u-viewport_lg */ 12 | @custom-media --u-viewport_min-lg (width > 1012px); /* --u-viewport_lg */ 13 | @custom-media --u-viewport_max-xl (width <= 1280px); /* --u-viewport_xl */ 14 | @custom-media --u-viewport_min-xl (width > 1280); /* --u-viewport_xl */ 15 | -------------------------------------------------------------------------------- /storybook/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100, 9 | "safari": 15, 10 | "firefox": 91 11 | } 12 | } 13 | ] 14 | ], 15 | "plugins": [] 16 | } 17 | -------------------------------------------------------------------------------- /storybook/.storybook/main.js: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require("postcss-preset-env"); 2 | const UI_CORE_SRC = "../src"; 3 | 4 | module.exports = { 5 | stories: ["../stories/**/*.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"], 6 | 7 | addons: [ 8 | "@storybook/addon-links", 9 | "@storybook/addon-essentials", 10 | "@storybook/addon-interactions", 11 | "storybook-dark-mode", 12 | "storybook-css-modules-preset", 13 | { 14 | name: "@storybook/addon-styling", 15 | options: { 16 | // These rules are the same as ones in unison-local-ui/webpack.dev.js 17 | cssBuildRule: { 18 | test: /\.css$/i, 19 | use: [ 20 | "style-loader", 21 | { 22 | loader: "css-loader", 23 | options: { importLoaders: 1 }, 24 | }, 25 | { 26 | loader: "postcss-loader", 27 | options: { 28 | postcssOptions: { 29 | plugins: [ 30 | postcssPresetEnv({ 31 | features: { 32 | "is-pseudo-class": false, 33 | "custom-media-queries": { 34 | importFrom: `${UI_CORE_SRC}/css/ui/viewport.css`, 35 | }, 36 | }, 37 | }), 38 | ], 39 | }, 40 | }, 41 | }, 42 | ], 43 | }, 44 | }, 45 | }, 46 | "@storybook/addon-webpack5-compiler-babel", 47 | "@chromatic-com/storybook", 48 | ], 49 | 50 | framework: { 51 | name: "@storybook/html-webpack5", 52 | options: {}, 53 | }, 54 | 55 | staticDirs: ["../static"], 56 | 57 | docs: {}, 58 | }; 59 | -------------------------------------------------------------------------------- /storybook/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /storybook/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import "../../src/css/code.css"; 2 | import "../../src/css/ui.css"; 3 | import "../../src/css/ui/core.css"; 4 | import "../../src/css/themes/unison-light.css"; 5 | 6 | import "../stories/Helpers/style.css"; 7 | 8 | export const parameters = { 9 | controls: { 10 | matchers: { 11 | color: /(background|color)$/i, 12 | date: /Date$/, 13 | }, 14 | }, 15 | }; 16 | export const tags = ["autodocs"]; 17 | -------------------------------------------------------------------------------- /storybook/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = async ({ config, mode }) => { 4 | config.resolve.extensions.push(".elm"); 5 | 6 | config.module.rules.push({ 7 | test: /\.elm$/, 8 | exclude: [/elm-stuff/, /node_modules/], 9 | loader: "elm-webpack-loader", 10 | options: { 11 | debug: mode === "DEVELOPMENT", 12 | }, 13 | }); 14 | 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /storybook/README.md: -------------------------------------------------------------------------------- 1 | # Unison-UICore Storybook 2 | 3 | Showing [Unison UI Core](https://github.com/unisonweb/ui-core) elements in [Storybook](https://storybook.js.org/). 4 | 5 | ## Usage 6 | 7 | ```sh 8 | $ npm install 9 | $ npm run storybook 10 | ``` 11 | 12 | ## Samples 13 | 14 | 15 | 16 | 17 | 18 | ## Reference 19 | 20 | - [Storybook and Elm](https://orangesodium.cc/ui/elm/2022/04/06/storybook-and-elm.html) 21 | -------------------------------------------------------------------------------- /storybook/doc/UICore-Storybook-Button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/storybook/doc/UICore-Storybook-Button.png -------------------------------------------------------------------------------- /storybook/doc/UICore-Storybook-Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/storybook/doc/UICore-Storybook-Icon.png -------------------------------------------------------------------------------- /storybook/doc/UICore-Storybook-WorkspaceItem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unisonweb/ui-core/08e6036c8d87b65bf602d1b138b395d839cabea3/storybook/doc/UICore-Storybook-WorkspaceItem.png -------------------------------------------------------------------------------- /storybook/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "../src", 5 | "stories" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "NoRedInk/elm-json-decode-pipeline": "1.0.1", 11 | "avh4/elm-color": "1.0.0", 12 | "elm/browser": "1.0.2", 13 | "elm/core": "1.0.5", 14 | "elm/html": "1.0.0", 15 | "elm/http": "2.0.0", 16 | "elm/json": "1.1.3", 17 | "elm/parser": "1.1.0", 18 | "elm/regex": "1.0.0", 19 | "elm/svg": "1.0.1", 20 | "elm/time": "1.0.0", 21 | "elm/url": "1.0.0", 22 | "elm-community/html-extra": "3.4.0", 23 | "elm-community/json-extra": "4.3.0", 24 | "elm-community/list-extra": "8.7.0", 25 | "elm-community/maybe-extra": "5.3.0", 26 | "elm-community/string-extra": "4.0.1", 27 | "justinmimbs/time-extra": "1.1.1", 28 | "krisajenkins/remotedata": "6.0.1", 29 | "mgold/elm-nonempty-list": "4.2.0", 30 | "noahzgordon/elm-color-extra": "1.0.2", 31 | "rtfeldman/elm-iso8601-date-strings": "1.1.4", 32 | "ryannhg/date-format": "2.3.0", 33 | "stoeffel/set-extra": "1.2.3", 34 | "wernerdegroot/listzipper": "4.0.0" 35 | }, 36 | "indirect": { 37 | "elm/bytes": "1.0.8", 38 | "elm/file": "1.0.5", 39 | "elm/random": "1.0.0", 40 | "elm/virtual-dom": "1.0.3", 41 | "fredcy/elm-parseint": "2.0.1", 42 | "justinmimbs/date": "4.0.1" 43 | } 44 | }, 45 | "test-dependencies": { 46 | "direct": {}, 47 | "indirect": {} 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unison-uicore-storybook", 3 | "scripts": { 4 | "start": "storybook dev -p 6006", 5 | "build": "storybook build" 6 | }, 7 | "license": "MIT", 8 | "devDependencies": { 9 | "@babel/core": "^7.21.0", 10 | "@babel/preset-env": "^7.22.10", 11 | "@chromatic-com/storybook": "^3.2.6", 12 | "@storybook/addon-actions": "^8.6.12", 13 | "@storybook/addon-essentials": "^8.6.12", 14 | "@storybook/addon-interactions": "^8.6.12", 15 | "@storybook/addon-links": "^8.6.12", 16 | "@storybook/addon-styling-webpack": "^1.0.1", 17 | "@storybook/addon-webpack5-compiler-babel": "^3.0.6", 18 | "@storybook/cli": "^8.6.12", 19 | "@storybook/html": "^8.6.12", 20 | "@storybook/html-webpack5": "^8.6.12", 21 | "@storybook/test": "^8.6.12", 22 | "babel-loader": "^8.3.0", 23 | "elm-webpack-loader": "^8.0.0", 24 | "postcss-preset-env": "^7.8.0", 25 | "storybook": "^8.6.12", 26 | "storybook-css-modules-preset": "^1.1.1", 27 | "storybook-dark-mode": "^4.0.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /storybook/stories/Helpers/Layout.elm: -------------------------------------------------------------------------------- 1 | module Helpers.Layout exposing (..) 2 | 3 | import Html exposing (Attribute, Html) 4 | import Html.Attributes exposing (class) 5 | 6 | 7 | columns : List (Attribute msg) -> List (Html msg) -> Html msg 8 | columns attrs children = 9 | Html.div (class "columns" :: attrs) children 10 | 11 | 12 | rows : List (Attribute msg) -> List (Html msg) -> Html msg 13 | rows attrs children = 14 | Html.div (class "rows" :: attrs) children 15 | -------------------------------------------------------------------------------- /storybook/stories/Helpers/ReferenceHelper.elm: -------------------------------------------------------------------------------- 1 | module Helpers.ReferenceHelper exposing (..) 2 | 3 | import Code.Definition.Reference exposing (..) 4 | import Code.FullyQualifiedName as FQN exposing (..) 5 | import Code.HashQualified exposing (..) 6 | 7 | 8 | sampleFQN : FQN 9 | sampleFQN = 10 | FQN.fromString "a.b.c" 11 | 12 | 13 | sampleHashQualified : HashQualified 14 | sampleHashQualified = 15 | NameOnly sampleFQN 16 | 17 | 18 | sampleReference : Reference 19 | sampleReference = 20 | TypeReference sampleHashQualified 21 | -------------------------------------------------------------------------------- /storybook/stories/Helpers/style.css: -------------------------------------------------------------------------------- 1 | .columns { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1rem; 5 | } 6 | 7 | .rows { 8 | display: flex; 9 | flex-direction: rows; 10 | gap: 1rem; 11 | } 12 | 13 | .padded { 14 | padding: 2rem; 15 | } 16 | -------------------------------------------------------------------------------- /storybook/stories/Stories/Code/code.stories.js: -------------------------------------------------------------------------------- 1 | export default { title: "Code" }; 2 | 3 | import { initElmStory } from "../../initElmStory.js"; 4 | import WorkspaceItem from "./WorkspaceItem.elm"; 5 | import Workspace from "./Workspace.elm"; 6 | import WorkspaceMinimap from "./WorkspaceMinimap.elm"; 7 | import PageContent from "./PageContent.elm"; 8 | 9 | export const workspace = () => { 10 | return initElmStory(Workspace.Elm.Stories.Code.Workspace); 11 | }; 12 | 13 | export const workspaceItem = () => { 14 | return initElmStory(WorkspaceItem.Elm.Stories.Code.WorkspaceItem); 15 | }; 16 | 17 | export const workspaceMinimap = () => { 18 | return initElmStory(WorkspaceMinimap.Elm.Stories.Code.WorkspaceMinimap); 19 | }; 20 | 21 | export const pageContent = () => { 22 | return initElmStory(PageContent.Elm.Stories.Code.PageContent); 23 | }; 24 | -------------------------------------------------------------------------------- /storybook/stories/Stories/Lib/MermaidDiagram.elm: -------------------------------------------------------------------------------- 1 | module Stories.Lib.MermaidDiagram exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html, text) 6 | import Lib.MermaidDiagram as MermaidDiagram 7 | 8 | 9 | main : Program () () Msg 10 | main = 11 | Browser.element 12 | { init = \_ -> ( (), Cmd.none ) 13 | , view = \_ -> view 14 | , update = \_ model -> ( model, Cmd.none ) 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | type alias Msg = 20 | () 21 | 22 | 23 | diagram : String 24 | diagram = 25 | """graph 26 | A-->B[Unison] 27 | B-->C[supports] 28 | C-->D[Mermaid] 29 | """ 30 | 31 | 32 | view : Html Msg 33 | view = 34 | columns [] 35 | [ text "Mermaid Diagram" 36 | , MermaidDiagram.mermaid diagram 37 | |> MermaidDiagram.view 38 | ] 39 | -------------------------------------------------------------------------------- /storybook/stories/Stories/Lib/lib.stories.js: -------------------------------------------------------------------------------- 1 | export default { title: "Lib" }; 2 | 3 | import { initElmStory } from "../../initElmStory.js"; 4 | import "../../../../src/Lib/MermaidDiagram.js"; // webcomponent 5 | import MermaidDiagram from "./MermaidDiagram.elm"; 6 | 7 | export const mermaid = () => { 8 | return initElmStory(MermaidDiagram.Elm.Stories.Lib.MermaidDiagram); 9 | }; 10 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Banner.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Banner exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.Banner as B 7 | import UI.Click as C 8 | 9 | 10 | main : Program () () Msg 11 | main = 12 | Browser.element 13 | { init = \_ -> ( (), Cmd.none ) 14 | , view = \_ -> view 15 | , update = \_ model -> ( model, Cmd.none ) 16 | , subscriptions = \_ -> Sub.none 17 | } 18 | 19 | 20 | type alias Msg = 21 | () 22 | 23 | 24 | elements : List (B.Banner Msg) 25 | elements = 26 | [ B.info "Info" 27 | , B.promotion "Promotion ID" "Content" (C.ExternalHref C.Blank "https://unison-lang.org") "Link" 28 | ] 29 | 30 | 31 | view : Html Msg 32 | view = 33 | columns [] (elements |> List.map B.view) 34 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Button.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Button exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.Button as B 7 | import UI.Icon as I 8 | 9 | 10 | main : Program () () Msg 11 | main = 12 | Browser.element 13 | { init = \_ -> ( (), Cmd.none ) 14 | , view = \_ -> view 15 | , update = \_ model -> ( model, Cmd.none ) 16 | , subscriptions = \_ -> Sub.none 17 | } 18 | 19 | 20 | type Msg 21 | = UserClicked 22 | 23 | 24 | elements : List (B.Button Msg) 25 | elements = 26 | [ B.button UserClicked "Button" 27 | , B.icon UserClicked I.unisonMark 28 | , B.iconThenLabel UserClicked I.unisonMark "Icon then Label" 29 | , B.labelThenIcon UserClicked "Label then Icon" I.unisonMark 30 | , B.iconThenLabelThenIcon UserClicked I.unisonMark "Icon then Label then Icon" I.caretDown 31 | ] 32 | 33 | 34 | view : Html Msg 35 | view = 36 | columns [] (elements |> List.map B.view) 37 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Card.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Card exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.Card as C 7 | 8 | 9 | main : Program () () Msg 10 | main = 11 | Browser.element 12 | { init = \_ -> ( (), Cmd.none ) 13 | , view = \_ -> view 14 | , update = \_ model -> ( model, Cmd.none ) 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | type alias Msg = 20 | () 21 | 22 | 23 | items : List (Html msg) 24 | items = 25 | [ Html.text "Inner text" 26 | ] 27 | 28 | 29 | elements : List (C.Card Msg) 30 | elements = 31 | [ C.card items 32 | , C.titled "Title" items 33 | ] 34 | 35 | 36 | view : Html Msg 37 | view = 38 | columns [] (elements |> List.map C.view) 39 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/CopyField.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.CopyField exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.CopyField as C 7 | 8 | 9 | main : Program () () Msg 10 | main = 11 | Browser.element 12 | { init = \_ -> ( (), Cmd.none ) 13 | , view = \_ -> view 14 | , update = \_ model -> ( model, Cmd.none ) 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | type Msg 20 | = Copied String 21 | 22 | 23 | elements : List (C.CopyField Msg) 24 | elements = 25 | [ C.copyField Copied "field" 26 | ] 27 | 28 | 29 | view : Html Msg 30 | view = 31 | columns [] (elements |> List.map C.view) 32 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/ErrorCard.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.ErrorCard exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.ErrorCard as E 7 | 8 | 9 | main : Program () () Msg 10 | main = 11 | Browser.element 12 | { init = \_ -> ( (), Cmd.none ) 13 | , view = \_ -> view 14 | , update = \_ model -> ( model, Cmd.none ) 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | type Msg 20 | = UserClicked 21 | 22 | 23 | elements : List (E.ErrorCard Msg) 24 | elements = 25 | [ E.empty 26 | , E.errorCard "Error Card Title" "Error Card Text" 27 | ] 28 | 29 | 30 | view : Html Msg 31 | view = 32 | columns [] (elements |> List.map E.view) 33 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/FoldToggle.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.FoldToggle exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.FoldToggle as F 7 | 8 | 9 | main : Program () () Msg 10 | main = 11 | Browser.element 12 | { init = \_ -> ( (), Cmd.none ) 13 | , view = \_ -> view 14 | , update = \_ model -> ( model, Cmd.none ) 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | type Msg 20 | = UserClicked 21 | 22 | 23 | elements : List (F.FoldToggle Msg) 24 | elements = 25 | [ F.foldToggle UserClicked 26 | , F.foldToggle UserClicked |> F.open 27 | , F.disabled 28 | ] 29 | 30 | 31 | view : Html Msg 32 | view = 33 | columns [] (elements |> List.map F.view) 34 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Form/TextField.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Form.TextField exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.Form.TextField as TextField 7 | import UI.Icon as I 8 | 9 | 10 | type alias Model = 11 | { value : String } 12 | 13 | 14 | main : Program () Model Msg 15 | main = 16 | Browser.element 17 | { init = always ( { value = "" }, Cmd.none ) 18 | , view = view 19 | , update = update 20 | , subscriptions = always Sub.none 21 | } 22 | 23 | 24 | type Msg 25 | = OnInput String 26 | 27 | 28 | update : Msg -> Model -> ( Model, Cmd Msg ) 29 | update msg model = 30 | case msg of 31 | OnInput val -> 32 | ( { model | value = val }, Cmd.none ) 33 | 34 | 35 | elements : Model -> List (TextField.TextField Msg) 36 | elements model = 37 | [ TextField.field OnInput "Some label" model.value 38 | |> TextField.withAutofocus 39 | |> TextField.withHelpText "Required. Ex. 'Add List.map'" 40 | ] 41 | 42 | 43 | view : Model -> Html Msg 44 | view model = 45 | columns [] (elements model |> List.map TextField.view) 46 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Icon.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Icon exposing (main) 2 | 3 | import Browser 4 | import Html exposing (..) 5 | import UI.Icon as I 6 | 7 | 8 | main : Program () () Msg 9 | main = 10 | Browser.element 11 | { init = \_ -> ( (), Cmd.none ) 12 | , view = \_ -> view 13 | , update = \_ model -> ( model, Cmd.none ) 14 | , subscriptions = \_ -> Sub.none 15 | } 16 | 17 | 18 | type alias Msg = 19 | () 20 | 21 | 22 | elements : List (I.Icon msg) 23 | elements = 24 | [ I.unisonMark 25 | , I.patch 26 | , I.dataConstructor 27 | , I.abilityConstructor 28 | , I.ability 29 | , I.test 30 | , I.doc 31 | , I.docs 32 | , I.term 33 | , I.type_ 34 | , I.search 35 | , I.caretDown 36 | , I.caretLeft 37 | , I.caretRight 38 | , I.caretUp 39 | , I.arrowDown 40 | , I.arrowLeft 41 | , I.arrowRight 42 | , I.arrowUp 43 | , I.arrowLeftUp 44 | , I.arrowsToLine 45 | , I.arrowsFromLine 46 | , I.checkmark 47 | , I.chevronDown 48 | , I.chevronUp 49 | , I.chevronLeft 50 | , I.chevronRight 51 | , I.browse 52 | , I.folder 53 | , I.folderOutlined 54 | , I.intoFolder 55 | , I.hash 56 | , I.plus 57 | , I.warn 58 | , I.x 59 | , I.dot 60 | , I.boldDot 61 | , I.largeDot 62 | , I.dots 63 | , I.dash 64 | , I.boldDash 65 | , I.github 66 | , I.twitter 67 | , I.slack 68 | , I.download 69 | , I.upload 70 | , I.list 71 | , I.tags 72 | , I.tagsOutlined 73 | , I.clipboard 74 | , I.user 75 | , I.cog 76 | , I.chest 77 | , I.pencilRuler 78 | , I.exitDoor 79 | , I.documentCertificate 80 | , I.documentCode 81 | , I.certificate 82 | , I.leftSidebarOn 83 | , I.leftSidebarOff 84 | , I.bulb 85 | , I.heart 86 | , I.heartOutline 87 | , I.star 88 | , I.starOutline 89 | , I.rocket 90 | , I.eye 91 | , I.eyeSlash 92 | , I.unfoldedMap 93 | , I.padlock 94 | , I.bug 95 | , I.tada 96 | , I.cloud 97 | , I.questionmark 98 | , I.keyboardKey 99 | , I.presentation 100 | , I.presentationSlash 101 | , I.window 102 | , I.keyboard 103 | , I.wireframeGlobe 104 | , I.mapPin 105 | , I.mail 106 | , I.graduationCap 107 | , I.unfoldedMap 108 | ] 109 | 110 | 111 | view : Html Msg 112 | view = 113 | div [] [ h1 [] (elements |> List.map I.view) ] 114 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/KeyboardShortcut.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.KeyboardShortcut exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import Lib.OperatingSystem 7 | import UI.KeyboardShortcut as K 8 | import UI.KeyboardShortcut.Key as Key 9 | 10 | 11 | main : Program () () Msg 12 | main = 13 | Browser.element 14 | { init = \_ -> ( (), Cmd.none ) 15 | , view = \_ -> view 16 | , update = \_ currentModel -> ( currentModel, Cmd.none ) 17 | , subscriptions = \_ -> Sub.none 18 | } 19 | 20 | 21 | type Msg 22 | = UserClicked 23 | 24 | 25 | items : List (Html msg) 26 | items = 27 | [ Html.text "Inner text" 28 | ] 29 | 30 | 31 | elements : List K.KeyboardShortcut 32 | elements = 33 | [ K.single Key.Shift 34 | , K.Sequence (Just Key.Alt) Key.Shift 35 | , K.Chord Key.Ctrl (Key.K Key.Lower) 36 | ] 37 | 38 | 39 | model : K.Model 40 | model = 41 | { key = Just Key.Ctrl 42 | , operatingSystem = Lib.OperatingSystem.MacOS 43 | } 44 | 45 | 46 | view : Html Msg 47 | view = 48 | columns [] (elements |> List.map (K.view model)) 49 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Modal.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Modal exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.Modal as M 7 | 8 | 9 | main : Program () () Msg 10 | main = 11 | Browser.element 12 | { init = \_ -> ( (), Cmd.none ) 13 | , view = \_ -> view 14 | , update = \_ currentModel -> ( currentModel, Cmd.none ) 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | type Msg 20 | = UserClicked 21 | 22 | 23 | elements : List (M.Modal Msg) 24 | elements = 25 | [ M.modal "Modal" UserClicked (M.Content (Html.text "Content")) 26 | , M.modal "Modal" UserClicked (M.CustomContent (Html.text "Custom Content")) 27 | 28 | -- ,M.modal "Modal" UserClicked (M.Content (Html.text "Content")) |> M.withHeader "Header" 29 | ] 30 | 31 | 32 | view : Html Msg 33 | view = 34 | columns [] (elements |> List.map M.view) 35 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Navigation.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Navigation exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.Click as C 7 | import UI.Icon as I 8 | import UI.Navigation as N 9 | import UI.Nudge 10 | import UI.Tooltip 11 | 12 | 13 | main : Program () () Msg 14 | main = 15 | Browser.element 16 | { init = \_ -> ( (), Cmd.none ) 17 | , view = \_ -> view 18 | , update = \_ currentModel -> ( currentModel, Cmd.none ) 19 | , subscriptions = \_ -> Sub.none 20 | } 21 | 22 | 23 | type Msg 24 | = UserClicked 25 | 26 | 27 | sampleNavItem : N.NavItem Msg 28 | sampleNavItem = 29 | N.navItem "some" (C.ExternalHref C.Self "some") 30 | 31 | 32 | sampleNavItems : List (N.NavItem Msg) 33 | sampleNavItems = 34 | [ C.ExternalHref C.Self "link 1" |> N.navItem "item 1" 35 | , C.ExternalHref C.Self "link 2" |> N.navItem "item 2" 36 | , C.ExternalHref C.Self "link 3" |> N.navItem "item 3" 37 | ] 38 | 39 | 40 | elements : List (N.Navigation Msg) 41 | elements = 42 | [ N.withNoSelectedItems 43 | sampleNavItems 44 | N.empty 45 | , N.withItems 46 | (List.take 2 sampleNavItems) 47 | (C.ExternalHref C.Self "link 3" |> N.navItem "item 3") 48 | (List.drop 3 sampleNavItems) 49 | N.empty 50 | , N.withNoSelectedItems 51 | (sampleNavItems 52 | |> List.map (N.navItemWithIcon I.unisonMark) 53 | ) 54 | N.empty 55 | , N.withNoSelectedItems 56 | (sampleNavItems 57 | |> List.map (N.navItemWithNudge (UI.Nudge.nudge |> UI.Nudge.pulsate)) 58 | ) 59 | N.empty 60 | , N.withNoSelectedItems 61 | (sampleNavItems 62 | |> List.map (N.navItemWithTooltip (UI.Tooltip.text "tooltip text" |> UI.Tooltip.tooltip)) 63 | ) 64 | N.empty 65 | ] 66 | 67 | 68 | view : Html Msg 69 | view = 70 | columns [] (elements |> List.map N.view) 71 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/PageHeader.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.PageHeader exposing (..) 2 | 3 | import Browser 4 | import Html exposing (Html, div, text) 5 | import UI.Click as Click 6 | import UI.Icon as Icon 7 | import UI.Navigation as Nav 8 | import UI.PageHeader as PageHeader exposing (PageHeader) 9 | 10 | 11 | type alias Model = 12 | { mobileNavIsOpen : Bool 13 | } 14 | 15 | 16 | main : Program () Model Msg 17 | main = 18 | Browser.element 19 | { init = \_ -> ( { mobileNavIsOpen = False }, Cmd.none ) 20 | , view = view 21 | , update = update 22 | , subscriptions = \_ -> Sub.none 23 | } 24 | 25 | 26 | type Msg 27 | = NoOp 28 | | ToggleMobileNav 29 | 30 | 31 | update : Msg -> Model -> ( Model, Cmd Msg ) 32 | update msg model = 33 | case msg of 34 | NoOp -> 35 | ( model, Cmd.none ) 36 | 37 | ToggleMobileNav -> 38 | ( { model | mobileNavIsOpen = not model.mobileNavIsOpen }, Cmd.none ) 39 | 40 | 41 | view : Model -> Html Msg 42 | view model = 43 | pageHeader model 44 | |> PageHeader.view 45 | 46 | 47 | pageHeader : Model -> PageHeader Msg 48 | pageHeader model = 49 | let 50 | click = 51 | Click.onClick NoOp 52 | 53 | context = 54 | { isActive = False 55 | , content = div [] [ text "Pageheader Context" ] 56 | , click = Just click 57 | } 58 | 59 | allNavItems_ = 60 | { code = 61 | Nav.navItem "Code" click 62 | |> Nav.navItemWithIcon Icon.ability 63 | , releases = 64 | Nav.navItem "Releases" click 65 | |> Nav.navItemWithIcon Icon.rocket 66 | , settings = 67 | Nav.navItem "Settings" click 68 | |> Nav.navItemWithIcon Icon.cog 69 | } 70 | 71 | nav = 72 | { navigation = 73 | Nav.withItems [] 74 | allNavItems_.code 75 | [ allNavItems_.releases, allNavItems_.settings ] 76 | Nav.empty 77 | , mobileNavToggleMsg = ToggleMobileNav 78 | , mobileNavIsOpen = model.mobileNavIsOpen 79 | } 80 | in 81 | context 82 | |> PageHeader.pageHeader 83 | |> PageHeader.withNavigation nav 84 | |> PageHeader.withRightSide [] 85 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Placeholder.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Placeholder exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import Html.Attributes exposing (class) 7 | import UI.Placeholder as Placeholder 8 | 9 | 10 | main : Program () () msg 11 | main = 12 | Browser.element 13 | { init = \_ -> ( (), Cmd.none ) 14 | , view = \_ -> view 15 | , update = \_ currentModel -> ( currentModel, Cmd.none ) 16 | , subscriptions = \_ -> Sub.none 17 | } 18 | 19 | 20 | elements : List (Html msg) 21 | elements = 22 | [ Placeholder.text |> Placeholder.view 23 | ] 24 | 25 | 26 | view : Html msg 27 | view = 28 | columns [ class "padded" ] elements 29 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/StatusBanner.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.StatusBanner exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html) 6 | import UI.StatusBanner as S 7 | 8 | 9 | main : Program () () msg 10 | main = 11 | Browser.element 12 | { init = \_ -> ( (), Cmd.none ) 13 | , view = \_ -> view 14 | , update = \_ currentModel -> ( currentModel, Cmd.none ) 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | elements : List (Html msg) 20 | elements = 21 | [ S.good "good text" 22 | , S.bad "bad text" 23 | , S.info "info text" 24 | , S.working "working text" 25 | ] 26 | 27 | 28 | view : Html msg 29 | view = 30 | elements 31 | |> columns [] 32 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/Tooltip.elm: -------------------------------------------------------------------------------- 1 | module Stories.UI.Tooltip exposing (..) 2 | 3 | import Browser 4 | import Helpers.Layout exposing (columns) 5 | import Html exposing (Html, span, text) 6 | import Html.Attributes exposing (class) 7 | import UI.Placeholder as Placeholder 8 | import UI.Tooltip as Tooltip 9 | 10 | 11 | main : Program () () msg 12 | main = 13 | Browser.element 14 | { init = \_ -> ( (), Cmd.none ) 15 | , view = \_ -> view 16 | , update = \_ currentModel -> ( currentModel, Cmd.none ) 17 | , subscriptions = \_ -> Sub.none 18 | } 19 | 20 | 21 | trigger : String -> Html msg 22 | trigger t = 23 | span [] [ text t ] 24 | 25 | 26 | elements : List (Html msg) 27 | elements = 28 | [ Tooltip.tooltip (Tooltip.text "Text tooltip") 29 | |> Tooltip.view (trigger "Text tooltip") 30 | , Tooltip.tooltip (Placeholder.text |> Placeholder.small |> Placeholder.view |> Tooltip.rich) 31 | |> Tooltip.view (trigger "Placeholder tooltip") 32 | ] 33 | 34 | 35 | view : Html msg 36 | view = 37 | columns [ class "padded" ] elements 38 | -------------------------------------------------------------------------------- /storybook/stories/Stories/UI/ui.stories.js: -------------------------------------------------------------------------------- 1 | export default { title: "UI" }; 2 | 3 | import { initElmStory } from "../../initElmStory.js"; 4 | import Icon from "./Icon.elm"; 5 | import Button from "./Button.elm"; 6 | import Card from "./Card.elm"; 7 | import ErrorCard from "./ErrorCard.elm"; 8 | import Banner from "./Banner.elm"; 9 | import CopyField from "./CopyField.elm"; 10 | import "../../../../src/UI/CopyOnClick.js"; // webcomponent 11 | import FoldToggle from "./FoldToggle.elm"; 12 | import KeyboardShortcut from "./KeyboardShortcut.elm"; 13 | import Modal from "./Modal.elm"; 14 | import Navigation from "./Navigation.elm"; 15 | import PageHeader from "./PageHeader.elm"; 16 | import Tooltip from "./Tooltip.elm"; 17 | import Placeholder from "./Placeholder.elm"; 18 | import StatusBanner from "./StatusBanner.elm"; 19 | import TextField from "./Form/TextField.elm"; 20 | 21 | export const banner = () => { 22 | return initElmStory(Banner.Elm.Stories.UI.Banner); 23 | }; 24 | 25 | export const button = () => { 26 | return initElmStory(Button.Elm.Stories.UI.Button); 27 | }; 28 | 29 | export const card = () => { 30 | return initElmStory(Card.Elm.Stories.UI.Card); 31 | }; 32 | 33 | export const copyField = () => { 34 | return initElmStory(CopyField.Elm.Stories.UI.CopyField); 35 | }; 36 | 37 | export const errorCard = () => { 38 | return initElmStory(ErrorCard.Elm.Stories.UI.ErrorCard); 39 | }; 40 | 41 | export const foldToggle = () => { 42 | return initElmStory(FoldToggle.Elm.Stories.UI.FoldToggle); 43 | }; 44 | 45 | export const icon = () => { 46 | return initElmStory(Icon.Elm.Stories.UI.Icon); 47 | }; 48 | 49 | export const keyboardShortcut = () => { 50 | return initElmStory(KeyboardShortcut.Elm.Stories.UI.KeyboardShortcut); 51 | }; 52 | 53 | export const modal = () => { 54 | return initElmStory(Modal.Elm.Stories.UI.Modal); 55 | }; 56 | 57 | export const navigation = () => { 58 | return initElmStory(Navigation.Elm.Stories.UI.Navigation); 59 | }; 60 | 61 | export const pageHeader = () => { 62 | return initElmStory(PageHeader.Elm.Stories.UI.PageHeader); 63 | }; 64 | 65 | export const tooltip = () => { 66 | return initElmStory(Tooltip.Elm.Stories.UI.Tooltip); 67 | }; 68 | 69 | export const placeholder = () => { 70 | return initElmStory(Placeholder.Elm.Stories.UI.Placeholder); 71 | }; 72 | 73 | export const statusBanner = () => { 74 | return initElmStory(StatusBanner.Elm.Stories.UI.StatusBanner); 75 | }; 76 | 77 | export const textField = () => { 78 | return initElmStory(TextField.Elm.Stories.UI.Form.TextField); 79 | }; 80 | -------------------------------------------------------------------------------- /storybook/stories/initElmStory.js: -------------------------------------------------------------------------------- 1 | export const initElmStory = (elmApp) => { 2 | const containerDiv = document.createElement("div"); 3 | const innerDiv = document.createElement("div"); 4 | 5 | containerDiv.appendChild(innerDiv); 6 | 7 | elmApp.init({ 8 | node: innerDiv, 9 | }); 10 | 11 | return containerDiv; 12 | }; 13 | -------------------------------------------------------------------------------- /tests/Code/Definition/InfoTests.elm: -------------------------------------------------------------------------------- 1 | module Code.Definition.InfoTests exposing (..) 2 | 3 | import Code.Definition.Info as Info 4 | import Code.Definition.Reference as Reference exposing (Reference(..)) 5 | import Code.FullyQualifiedName as FQN 6 | import Expect 7 | import List.Nonempty as NEL 8 | import Test exposing (..) 9 | 10 | 11 | namespaceAndOtherNames : Test 12 | namespaceAndOtherNames = 13 | describe "Info.namespaceAndOtherNames" 14 | [ test "does not have duplicates in otherNames" <| 15 | \_ -> 16 | let 17 | result = 18 | Info.namespaceAndOtherNames 19 | (Reference.fromString TermReference "List.map") 20 | (FQN.fromString "map") 21 | (NEL.Nonempty 22 | (FQN.fromString "List.map") 23 | [ FQN.fromString "base.List.map", FQN.fromString "something.else.List.map", FQN.fromString "base.List.map" ] 24 | ) 25 | 26 | otherNames = 27 | Tuple.second result |> List.map FQN.toString 28 | 29 | expected = 30 | [ "base.List.map", "something.else.List.map" ] 31 | in 32 | Expect.equal expected otherNames 33 | ] 34 | -------------------------------------------------------------------------------- /tests/Code/ProjectDependencyTests.elm: -------------------------------------------------------------------------------- 1 | module Code.ProjectDependencyTests exposing (..) 2 | 3 | import Code.ProjectDependency as ProjectDependency 4 | import Expect 5 | import Test exposing (..) 6 | 7 | 8 | fromString : Test 9 | fromString = 10 | describe "ProjectDependency.fromString" 11 | [ test "parses 'base'" <| 12 | \_ -> 13 | Expect.equal 14 | "base" 15 | (ProjectDependency.toString (ProjectDependency.fromString "base")) 16 | , test "parses 'unison_base'" <| 17 | \_ -> 18 | Expect.equal 19 | "@unison/base" 20 | (ProjectDependency.toString (ProjectDependency.fromString "unison_base")) 21 | , test "parses 'unison_base_with_underscore'" <| 22 | \_ -> 23 | Expect.equal 24 | "unison_base_with_underscore" 25 | (ProjectDependency.toString (ProjectDependency.fromString "unison_base_with_underscore")) 26 | , test "parses 'base_1_2_3'" <| 27 | \_ -> 28 | Expect.equal 29 | "base v1.2.3" 30 | (ProjectDependency.toString (ProjectDependency.fromString "base_1_2_3")) 31 | , test "parses 'unison_base_1_2_3'" <| 32 | \_ -> 33 | Expect.equal 34 | "@unison/base v1.2.3" 35 | (ProjectDependency.toString (ProjectDependency.fromString "unison_base_1_2_3")) 36 | , test "parses 'unison_base_with_underscore_1_2_3'" <| 37 | \_ -> 38 | Expect.equal 39 | "unison_base_with_underscore v1.2.3" 40 | (ProjectDependency.toString (ProjectDependency.fromString "unison_base_with_underscore_1_2_3")) 41 | ] 42 | -------------------------------------------------------------------------------- /tests/Code/ProjectNameTests.elm: -------------------------------------------------------------------------------- 1 | module Code.ProjectNameTests exposing (..) 2 | 3 | import Code.ProjectName as ProjectName exposing (ProjectName) 4 | import Code.ProjectSlug as ProjectSlug 5 | import Expect 6 | import Lib.UserHandle as UserHandle 7 | import Test exposing (..) 8 | 9 | 10 | toString : Test 11 | toString = 12 | describe "ProjectName.toString" 13 | [ test "Formats the ref to a string" <| 14 | \_ -> 15 | Expect.equal 16 | "@unison/http" 17 | (ProjectName.toString projectName) 18 | ] 19 | 20 | 21 | fromString : Test 22 | fromString = 23 | describe "ProjectName.fromString" 24 | [ test "creates a ProjectName from valid handle string and a valid slug string" <| 25 | \_ -> 26 | let 27 | result = 28 | ProjectName.fromString "@unison/http" 29 | |> Maybe.map ProjectName.toString 30 | |> Maybe.withDefault "FAIL" 31 | in 32 | Expect.equal "@unison/http" result 33 | ] 34 | 35 | 36 | unsafeFromString : Test 37 | unsafeFromString = 38 | describe "ProjectName.unsafeFromString" 39 | [ test "creates a ProjectName from valid unprefixed handle string and a valid slug string" <| 40 | \_ -> 41 | let 42 | result = 43 | ProjectName.unsafeFromString "unison/http" 44 | |> ProjectName.toString 45 | in 46 | Expect.equal "@unison/http" result 47 | ] 48 | 49 | 50 | handle : Test 51 | handle = 52 | describe "ProjectName.handle" 53 | [ test "Extracts the handle" <| 54 | \_ -> 55 | Expect.equal 56 | "@unison" 57 | (ProjectName.handle projectName |> Maybe.map UserHandle.toString |> Maybe.withDefault "FAIL") 58 | ] 59 | 60 | 61 | slug : Test 62 | slug = 63 | describe "ProjectName.slug" 64 | [ test "Extracts the slug" <| 65 | \_ -> 66 | Expect.equal 67 | "http" 68 | (ProjectName.slug projectName |> ProjectSlug.toString) 69 | ] 70 | 71 | 72 | 73 | -- Helpers 74 | 75 | 76 | projectName : ProjectName 77 | projectName = 78 | let 79 | handle_ = 80 | UserHandle.unsafeFromString "unison" 81 | 82 | slug_ = 83 | ProjectSlug.unsafeFromString "http" 84 | in 85 | ProjectName.projectName (Just handle_) slug_ 86 | -------------------------------------------------------------------------------- /tests/Lib/HttpApiTests.elm: -------------------------------------------------------------------------------- 1 | module Lib.HttpApiTests exposing (..) 2 | 3 | import Expect 4 | import Lib.HttpApi as HttpApi 5 | import Test exposing (..) 6 | import Url.Builder exposing (string) 7 | 8 | 9 | toUrl : Test 10 | toUrl = 11 | describe "HttpApi.toUrl" 12 | [ test "Returns a full URL string with endpoint path and query params" <| 13 | \_ -> 14 | let 15 | apiUrl = 16 | HttpApi.apiUrlFromString False "https://api.example.com" 17 | 18 | endpoint = 19 | HttpApi.GET 20 | { path = [ "some", "path" ] 21 | , queryParams = [ string "search" "param" ] 22 | } 23 | 24 | result = 25 | HttpApi.toUrl apiUrl endpoint 26 | in 27 | Expect.equal "https://api.example.com/some/path?search=param" result 28 | ] 29 | -------------------------------------------------------------------------------- /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/UI/DateTimeTests.elm: -------------------------------------------------------------------------------- 1 | module UI.DateTimeTests exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | import Time 6 | import UI.DateTime as DateTime exposing (DateTimeFormat(..)) 7 | 8 | 9 | fromString : Test 10 | fromString = 11 | describe "DateTime.fromISO8601" 12 | [ test "Creates a DateTime from a valid ISO8601 string" <| 13 | \_ -> 14 | "2023-08-15T15:00:00.998Z" 15 | |> DateTime.fromISO8601 16 | |> Maybe.map DateTime.toISO8601 17 | |> Expect.equal (Just "2023-08-15T15:00:00.998Z") 18 | , test "Fails to create from an invalid string" <| 19 | \_ -> 20 | DateTime.fromISO8601 "-----" 21 | |> Expect.equal Nothing 22 | ] 23 | 24 | 25 | toString : Test 26 | toString = 27 | describe "DateTime.toString" 28 | [ test "with format FullDateTime" <| 29 | \_ -> 30 | "2023-08-15T15:00:00.998Z" 31 | |> DateTime.fromISO8601 32 | |> Maybe.map (DateTime.toString FullDateTime Time.utc) 33 | |> Expect.equal (Just "Aug 15, 2023 - 15:00:00") 34 | , test "with format ShortDate" <| 35 | \_ -> 36 | "2023-08-15T15:00:00.998Z" 37 | |> DateTime.fromISO8601 38 | |> Maybe.map (DateTime.toString ShortDate Time.utc) 39 | |> Expect.equal (Just "Aug 15, 2023") 40 | , test "with format LongDate" <| 41 | \_ -> 42 | "2023-08-15T15:00:00.998Z" 43 | |> DateTime.fromISO8601 44 | |> Maybe.map (DateTime.toString LongDate Time.utc) 45 | |> Expect.equal (Just "August 15, 2023") 46 | , test "with format TimeWithSeconds24Hour" <| 47 | \_ -> 48 | "2023-08-15T15:00:00.998Z" 49 | |> DateTime.fromISO8601 50 | |> Maybe.map (DateTime.toString TimeWithSeconds24Hour Time.utc) 51 | |> Expect.equal (Just "15:00:00") 52 | , test "with format TimeWithSeconds12Hour" <| 53 | \_ -> 54 | "2023-08-15T15:00:00.998Z" 55 | |> DateTime.fromISO8601 56 | |> Maybe.map (DateTime.toString TimeWithSeconds12Hour Time.utc) 57 | |> Expect.equal (Just "3:00:00 pm") 58 | ] 59 | --------------------------------------------------------------------------------