├── .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 | [](https://github.com/unisonweb/ui-core/actions/workflows/ci.yml)
6 |
7 | 
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 |
21 |
--------------------------------------------------------------------------------
/src/css/ui/empty-state_search_on-light.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------