From 47a02e5611004f121f30e6a6ab92a50f028b84ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8jberg?= Date: Fri, 3 May 2024 15:12:38 -0400 Subject: [PATCH] =?UTF-8?q?Open=20source=20Share=20UI=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 11 + .github/workflows/ci.yml | 30 + .github/workflows/playwright.yml | 27 + .gitignore | 15 + .prettierignore | 12 + .prettierrc.json | 1 + LICENSE | 19 + README.md | 40 + elm-git.json | 8 + elm.json | 55 + netlify.toml | 4 + package-lock.json | 14590 ++++++++++++++++ package.json | 61 + playwright.config.ts | 78 + review/elm.json | 38 + review/src/ReviewConfig.elm | 20 + review/suppressed/NoUnused.Dependencies.json | 8 + review/suppressed/NoUnused.Modules.json | 9 + src/404.html | 103 + src/500.html | 106 + src/UnisonShare.elm | 18 + src/UnisonShare/Account.elm | 114 + src/UnisonShare/Api.elm | 1134 ++ src/UnisonShare/App.elm | 715 + src/UnisonShare/AppContext.elm | 66 + src/UnisonShare/AppDocument.elm | 108 + src/UnisonShare/AppError.elm | 36 + src/UnisonShare/AppHeader.elm | 331 + src/UnisonShare/BranchSummary.elm | 14 + src/UnisonShare/Catalog.elm | 88 + src/UnisonShare/CodeBrowsingContext.elm | 33 + src/UnisonShare/CodebaseStatus.elm | 44 + src/UnisonShare/Contribution.elm | 58 + .../Contribution/ContributionEvent.elm | 52 + .../Contribution/ContributionRef.elm | 96 + .../Contribution/ContributionStatus.elm | 45 + src/UnisonShare/ContributionTimeline.elm | 533 + src/UnisonShare/DateTimeContext.elm | 11 + src/UnisonShare/Diff.elm | 222 + src/UnisonShare/InteractiveDoc.elm | 82 + src/UnisonShare/Link.elm | 298 + src/UnisonShare/Log.elm | 21 + src/UnisonShare/Metrics.elm | 27 + src/UnisonShare/Page/AcceptTermsPage.elm | 143 + src/UnisonShare/Page/AccountPage.elm | 261 + src/UnisonShare/Page/AppErrorPage.elm | 69 + src/UnisonShare/Page/CatalogPage.elm | 586 + src/UnisonShare/Page/CloudPage.elm | 48 + src/UnisonShare/Page/CodePage.elm | 595 + src/UnisonShare/Page/CodePageContent.elm | 338 + src/UnisonShare/Page/ErrorPage.elm | 39 + src/UnisonShare/Page/NotFoundPage.elm | 35 + src/UnisonShare/Page/PrivacyPolicyPage.elm | 38 + src/UnisonShare/Page/ProjectBranchesPage.elm | 366 + .../Page/ProjectContributionChangesPage.elm | 365 + .../Page/ProjectContributionOverviewPage.elm | 442 + .../Page/ProjectContributionPage.elm | 429 + .../Page/ProjectContributionsPage.elm | 433 + src/UnisonShare/Page/ProjectOverviewPage.elm | 893 + src/UnisonShare/Page/ProjectPage.elm | 1579 ++ src/UnisonShare/Page/ProjectPageHeader.elm | 309 + src/UnisonShare/Page/ProjectReleasePage.elm | 433 + src/UnisonShare/Page/ProjectReleasesPage.elm | 890 + src/UnisonShare/Page/ProjectSettingsPage.elm | 299 + src/UnisonShare/Page/ProjectTicketPage.elm | 465 + src/UnisonShare/Page/ProjectTicketsPage.elm | 323 + src/UnisonShare/Page/TermsOfServicePage.elm | 41 + src/UnisonShare/Page/UcmConnectedPage.elm | 47 + .../Page/UserContributionsPage.elm | 456 + src/UnisonShare/Page/UserPage.elm | 512 + src/UnisonShare/Page/UserProfilePage.elm | 359 + src/UnisonShare/PageFooter.elm | 14 + src/UnisonShare/PreApp.elm | 224 + src/UnisonShare/Project.elm | 265 + src/UnisonShare/Project/ProjectDependency.elm | 31 + src/UnisonShare/Project/ProjectListing.elm | 75 + src/UnisonShare/Project/ProjectRef.elm | 165 + src/UnisonShare/Project/Release.elm | 130 + src/UnisonShare/Project/ReleaseDownloads.elm | 36 + .../ProjectContributionFormModal.elm | 659 + src/UnisonShare/ProjectTicketFormModal.elm | 325 + .../PublishProjectReleaseModal.elm | 611 + src/UnisonShare/Route.elm | 890 + src/UnisonShare/SearchBranchSheet.elm | 307 + src/UnisonShare/Session.elm | 109 + src/UnisonShare/SetupInstructions.elm | 227 + src/UnisonShare/SupportChatWidget.elm | 20 + src/UnisonShare/SupportChatWidget.js | 42 + src/UnisonShare/SwitchBranch.elm | 254 + src/UnisonShare/Ticket.elm | 40 + src/UnisonShare/Ticket/TicketEvent.elm | 52 + src/UnisonShare/Ticket/TicketRef.elm | 96 + src/UnisonShare/Ticket/TicketStatus.elm | 35 + src/UnisonShare/Ticket/TicketTimeline.elm | 528 + src/UnisonShare/Timeline/CommentEvent.elm | 377 + src/UnisonShare/Timeline/CommentId.elm | 27 + .../Timeline/StatusChangeEvent.elm | 24 + src/UnisonShare/Timeline/TimelineEvent.elm | 41 + src/UnisonShare/Tour.elm | 25 + src/UnisonShare/UnisonRelease.elm | 84 + src/UnisonShare/User.elm | 99 + src/UnisonShare/UserPageHeader.elm | 85 + src/WebsiteApi.elm | 8 + src/WhatsNew.elm | 143 + src/assets/circle-grid-color.svg | 1 + src/assets/confetti.svg | 30 + src/assets/dev-favicon.svg | 4 + src/assets/favicon.svg | 11 + src/assets/unison-cloud-splash.svg | 1399 ++ src/assets/unison-logo-circle.png | Bin 0 -> 42547 bytes src/assets/unison-logo-square.png | Bin 0 -> 31880 bytes src/assets/unison-share-social.png | Bin 0 -> 51420 bytes src/assets/user-profile-empty-state-wave.svg | 65 + src/css/unison-share.css | 18 + src/css/unison-share/app.css | 238 + src/css/unison-share/banner.css | 48 + src/css/unison-share/download-modal.css | 18 + src/css/unison-share/help-modal.css | 36 + src/css/unison-share/info-modal.css | 31 + src/css/unison-share/page.css | 20 + .../unison-share/page/accept-terms-page.css | 16 + src/css/unison-share/page/catalog-page.css | 384 + src/css/unison-share/page/cloud-page.css | 22 + src/css/unison-share/page/code-page.css | 89 + src/css/unison-share/page/error-page.css | 6 + .../page/project-branches-page.css | 113 + .../project-contribution-changes-page.css | 158 + .../project-contribution-overview-page.css | 108 + .../page/project-contribution-page.css | 102 + .../page/project-contributions-page.css | 59 + .../page/project-overview-page.css | 198 + src/css/unison-share/page/project-page.css | 94 + .../page/project-release-page.css | 23 + .../page/project-releases-page.css | 227 + .../page/project-settings-page.css | 99 + .../unison-share/page/project-ticket-page.css | 157 + .../page/project-tickets-page.css | 55 + src/css/unison-share/page/ucm-connected.css | 30 + .../page/user-contributions-page.css | 129 + .../unison-share/page/user-profile-page.css | 156 + .../project-contribution-form-modal.css | 79 + .../project-ticket-form-modal.css | 24 + .../unison-share/project/project-listing.css | 27 + src/css/unison-share/project/project-ref.css | 29 + .../publish-project-release-modal.css | 306 + src/css/unison-share/readme-card.css | 29 + src/css/unison-share/report-bug-modal.css | 21 + src/css/unison-share/search-branch-sheet.css | 93 + src/css/unison-share/setup-instructions.css | 79 + src/css/unison-share/timeline.css | 111 + src/css/unison-share/use-project-modal.css | 77 + src/css/unison-share/welcome-tour-modal.css | 135 + src/maintenance.html | 106 + src/metrics.js | 31 + src/privacy-policy.md | 180 + src/robots.txt | 4 + src/sitemap.txt | 2 + src/terms-of-service.md | 149 + src/unisonShare.ejs | 111 + src/unisonShare.js | 113 + src/util.js | 12 + .../Contribution/ContributionRefTests.elm | 59 + tests/UnisonShare/Project/ProjectRefTests.elm | 85 + .../Project/ReleaseDownloadsTest.elm | 56 + tests/UnisonShare/ProjectTests.elm | 106 + tests/UnisonShare/RouteTests.elm | 453 + tests/UnisonShare/Ticket/TicketRefTests.elm | 59 + tests/e2e/catalog.spec.ts | 73 + webpack.dev.js | 133 + webpack.prod.js | 158 + 170 files changed, 43266 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/playwright.yml create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 elm-git.json create mode 100644 elm.json create mode 100644 netlify.toml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 review/elm.json create mode 100644 review/src/ReviewConfig.elm create mode 100644 review/suppressed/NoUnused.Dependencies.json create mode 100644 review/suppressed/NoUnused.Modules.json create mode 100644 src/404.html create mode 100644 src/500.html create mode 100644 src/UnisonShare.elm create mode 100644 src/UnisonShare/Account.elm create mode 100644 src/UnisonShare/Api.elm create mode 100644 src/UnisonShare/App.elm create mode 100644 src/UnisonShare/AppContext.elm create mode 100644 src/UnisonShare/AppDocument.elm create mode 100644 src/UnisonShare/AppError.elm create mode 100644 src/UnisonShare/AppHeader.elm create mode 100644 src/UnisonShare/BranchSummary.elm create mode 100644 src/UnisonShare/Catalog.elm create mode 100644 src/UnisonShare/CodeBrowsingContext.elm create mode 100644 src/UnisonShare/CodebaseStatus.elm create mode 100644 src/UnisonShare/Contribution.elm create mode 100644 src/UnisonShare/Contribution/ContributionEvent.elm create mode 100644 src/UnisonShare/Contribution/ContributionRef.elm create mode 100644 src/UnisonShare/Contribution/ContributionStatus.elm create mode 100644 src/UnisonShare/ContributionTimeline.elm create mode 100644 src/UnisonShare/DateTimeContext.elm create mode 100644 src/UnisonShare/Diff.elm create mode 100644 src/UnisonShare/InteractiveDoc.elm create mode 100644 src/UnisonShare/Link.elm create mode 100644 src/UnisonShare/Log.elm create mode 100644 src/UnisonShare/Metrics.elm create mode 100644 src/UnisonShare/Page/AcceptTermsPage.elm create mode 100644 src/UnisonShare/Page/AccountPage.elm create mode 100644 src/UnisonShare/Page/AppErrorPage.elm create mode 100644 src/UnisonShare/Page/CatalogPage.elm create mode 100644 src/UnisonShare/Page/CloudPage.elm create mode 100644 src/UnisonShare/Page/CodePage.elm create mode 100644 src/UnisonShare/Page/CodePageContent.elm create mode 100644 src/UnisonShare/Page/ErrorPage.elm create mode 100644 src/UnisonShare/Page/NotFoundPage.elm create mode 100644 src/UnisonShare/Page/PrivacyPolicyPage.elm create mode 100644 src/UnisonShare/Page/ProjectBranchesPage.elm create mode 100644 src/UnisonShare/Page/ProjectContributionChangesPage.elm create mode 100644 src/UnisonShare/Page/ProjectContributionOverviewPage.elm create mode 100644 src/UnisonShare/Page/ProjectContributionPage.elm create mode 100644 src/UnisonShare/Page/ProjectContributionsPage.elm create mode 100644 src/UnisonShare/Page/ProjectOverviewPage.elm create mode 100644 src/UnisonShare/Page/ProjectPage.elm create mode 100644 src/UnisonShare/Page/ProjectPageHeader.elm create mode 100644 src/UnisonShare/Page/ProjectReleasePage.elm create mode 100644 src/UnisonShare/Page/ProjectReleasesPage.elm create mode 100644 src/UnisonShare/Page/ProjectSettingsPage.elm create mode 100644 src/UnisonShare/Page/ProjectTicketPage.elm create mode 100644 src/UnisonShare/Page/ProjectTicketsPage.elm create mode 100644 src/UnisonShare/Page/TermsOfServicePage.elm create mode 100644 src/UnisonShare/Page/UcmConnectedPage.elm create mode 100644 src/UnisonShare/Page/UserContributionsPage.elm create mode 100644 src/UnisonShare/Page/UserPage.elm create mode 100644 src/UnisonShare/Page/UserProfilePage.elm create mode 100644 src/UnisonShare/PageFooter.elm create mode 100644 src/UnisonShare/PreApp.elm create mode 100644 src/UnisonShare/Project.elm create mode 100644 src/UnisonShare/Project/ProjectDependency.elm create mode 100644 src/UnisonShare/Project/ProjectListing.elm create mode 100644 src/UnisonShare/Project/ProjectRef.elm create mode 100644 src/UnisonShare/Project/Release.elm create mode 100644 src/UnisonShare/Project/ReleaseDownloads.elm create mode 100644 src/UnisonShare/ProjectContributionFormModal.elm create mode 100644 src/UnisonShare/ProjectTicketFormModal.elm create mode 100644 src/UnisonShare/PublishProjectReleaseModal.elm create mode 100644 src/UnisonShare/Route.elm create mode 100644 src/UnisonShare/SearchBranchSheet.elm create mode 100644 src/UnisonShare/Session.elm create mode 100644 src/UnisonShare/SetupInstructions.elm create mode 100644 src/UnisonShare/SupportChatWidget.elm create mode 100644 src/UnisonShare/SupportChatWidget.js create mode 100644 src/UnisonShare/SwitchBranch.elm create mode 100644 src/UnisonShare/Ticket.elm create mode 100644 src/UnisonShare/Ticket/TicketEvent.elm create mode 100644 src/UnisonShare/Ticket/TicketRef.elm create mode 100644 src/UnisonShare/Ticket/TicketStatus.elm create mode 100644 src/UnisonShare/Ticket/TicketTimeline.elm create mode 100644 src/UnisonShare/Timeline/CommentEvent.elm create mode 100644 src/UnisonShare/Timeline/CommentId.elm create mode 100644 src/UnisonShare/Timeline/StatusChangeEvent.elm create mode 100644 src/UnisonShare/Timeline/TimelineEvent.elm create mode 100644 src/UnisonShare/Tour.elm create mode 100644 src/UnisonShare/UnisonRelease.elm create mode 100644 src/UnisonShare/User.elm create mode 100644 src/UnisonShare/UserPageHeader.elm create mode 100644 src/WebsiteApi.elm create mode 100644 src/WhatsNew.elm create mode 100644 src/assets/circle-grid-color.svg create mode 100644 src/assets/confetti.svg create mode 100644 src/assets/dev-favicon.svg create mode 100644 src/assets/favicon.svg create mode 100644 src/assets/unison-cloud-splash.svg create mode 100644 src/assets/unison-logo-circle.png create mode 100644 src/assets/unison-logo-square.png create mode 100644 src/assets/unison-share-social.png create mode 100644 src/assets/user-profile-empty-state-wave.svg create mode 100644 src/css/unison-share.css create mode 100644 src/css/unison-share/app.css create mode 100644 src/css/unison-share/banner.css create mode 100644 src/css/unison-share/download-modal.css create mode 100644 src/css/unison-share/help-modal.css create mode 100644 src/css/unison-share/info-modal.css create mode 100644 src/css/unison-share/page.css create mode 100644 src/css/unison-share/page/accept-terms-page.css create mode 100644 src/css/unison-share/page/catalog-page.css create mode 100644 src/css/unison-share/page/cloud-page.css create mode 100644 src/css/unison-share/page/code-page.css create mode 100644 src/css/unison-share/page/error-page.css create mode 100644 src/css/unison-share/page/project-branches-page.css create mode 100644 src/css/unison-share/page/project-contribution-changes-page.css create mode 100644 src/css/unison-share/page/project-contribution-overview-page.css create mode 100644 src/css/unison-share/page/project-contribution-page.css create mode 100644 src/css/unison-share/page/project-contributions-page.css create mode 100644 src/css/unison-share/page/project-overview-page.css create mode 100644 src/css/unison-share/page/project-page.css create mode 100644 src/css/unison-share/page/project-release-page.css create mode 100644 src/css/unison-share/page/project-releases-page.css create mode 100644 src/css/unison-share/page/project-settings-page.css create mode 100644 src/css/unison-share/page/project-ticket-page.css create mode 100644 src/css/unison-share/page/project-tickets-page.css create mode 100644 src/css/unison-share/page/ucm-connected.css create mode 100644 src/css/unison-share/page/user-contributions-page.css create mode 100644 src/css/unison-share/page/user-profile-page.css create mode 100644 src/css/unison-share/project-contribution-form-modal.css create mode 100644 src/css/unison-share/project-ticket-form-modal.css create mode 100644 src/css/unison-share/project/project-listing.css create mode 100644 src/css/unison-share/project/project-ref.css create mode 100644 src/css/unison-share/publish-project-release-modal.css create mode 100644 src/css/unison-share/readme-card.css create mode 100644 src/css/unison-share/report-bug-modal.css create mode 100644 src/css/unison-share/search-branch-sheet.css create mode 100644 src/css/unison-share/setup-instructions.css create mode 100644 src/css/unison-share/timeline.css create mode 100644 src/css/unison-share/use-project-modal.css create mode 100644 src/css/unison-share/welcome-tour-modal.css create mode 100644 src/maintenance.html create mode 100644 src/metrics.js create mode 100644 src/privacy-policy.md create mode 100644 src/robots.txt create mode 100644 src/sitemap.txt create mode 100644 src/terms-of-service.md create mode 100644 src/unisonShare.ejs create mode 100644 src/unisonShare.js create mode 100644 src/util.js create mode 100644 tests/UnisonShare/Contribution/ContributionRefTests.elm create mode 100644 tests/UnisonShare/Project/ProjectRefTests.elm create mode 100644 tests/UnisonShare/Project/ReleaseDownloadsTest.elm create mode 100644 tests/UnisonShare/ProjectTests.elm create mode 100644 tests/UnisonShare/RouteTests.elm create mode 100644 tests/UnisonShare/Ticket/TicketRefTests.elm create mode 100644 tests/e2e/catalog.spec.ts create mode 100644 webpack.dev.js create mode 100644 webpack.prod.js diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..619fcef6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +## Problem + +Describe the problem this PR intend to solve + +## Solution + +Describe the solution this PR takes to solve the problem and any alternative approach considered + +## Caveats/Notes + +Issues that address things you didn't get to or general notes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..16f5a432 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run ui-core-install + - run: npm run ui-core-check-css + - run: npm run build + - run: npm test + - run: npm run review + - run: npx prettier --check . diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..d4cf6dc9 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a5a31a2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +elm-stuff +.DS_Store +public/bundle.js +node_modules +dist +.unisonHistory + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +# Local Netlify folder +.netlify diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..014c1398 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +elm-stuff +public/bundle.js +node_modules +dist +.unison +.netlify +# apparently prettier adds a newline to all files and this clashes with +# autogenerated Elm JSON files: +# https://github.com/prettier/prettier/issues/6360 +elm-git.json +elm.json +review/suppressed/*.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..05a7ea78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022-2024, Unison Computing, public benefit corp and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..c428f035 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Unison Share UI + +[![CI](https://github.com/unisoncomputing/unison-share-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/unisoncomputing/unison-share-ui/actions/workflows/ci.yml) + +## Running Development Server + +Prerequisites: `node v20` or higher. + +1. Start [`Share API`](https://github.com/unisoncomputing/share-api) +2. Make sure the latest dependencies are installed with by running `npm +install`. +3. Start the dev server with: `API_URL="" npm start` + +Note that Share API hosts on `http://localhost:5424` be default, so its +likely what you'll run with, but you can always see the URL when starting +Share API. + +4. Visit `http://localhost:1234` in a browser. + +## Dependencies + +This depends on the [ui-core package](https://github.com/unisonweb/ui-core) via +[elm-git-install](https://github.com/robinheghan/elm-git-install). That package +includes both the Unison design system, and a core set of components for +working with and rendering Unison definitions and +namespaces. + +## Bumping [ui-core package](https://github.com/unisonweb/ui-core) + +The UI Core dependency can be updated to its latest version with this command: + +```bash +npm run ui-core-update +``` + +To install a specific sha: + +```bash +npm run ui-core-install -- [SOME_UI_CORE_SHA] +``` diff --git a/elm-git.json b/elm-git.json new file mode 100644 index 00000000..e536b2cf --- /dev/null +++ b/elm-git.json @@ -0,0 +1,8 @@ +{ + "git-dependencies": { + "direct": { + "https://github.com/unisonweb/ui-core": "45d0fd3e7c78c45d9713959a4a4b1e5e0b8f8e24" + }, + "indirect": {} + } +} \ No newline at end of file diff --git a/elm.json b/elm.json new file mode 100644 index 00000000..790d5b58 --- /dev/null +++ b/elm.json @@ -0,0 +1,55 @@ +{ + "type": "application", + "source-directories": [ + "elm-stuff/gitdeps/github.com/unisonweb/ui-core/src", + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "NoRedInk/elm-json-decode-pipeline": "1.0.1", + "NoRedInk/elm-simple-fuzzy": "1.0.3", + "avh4/elm-color": "1.0.0", + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/http": "2.0.0", + "elm/json": "1.1.3", + "elm/parser": "1.1.0", + "elm/regex": "1.0.0", + "elm/svg": "1.0.1", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.3", + "elm-community/html-extra": "3.4.0", + "elm-community/json-extra": "4.3.0", + "elm-community/list-extra": "8.7.0", + "elm-community/list-split": "1.0.3", + "elm-community/maybe-extra": "5.3.0", + "elm-community/string-extra": "4.0.1", + "elm-explorations/markdown": "1.0.0", + "j-maas/elm-ordered-containers": "1.0.0", + "justinmimbs/time-extra": "1.1.1", + "krisajenkins/remotedata": "6.0.1", + "mgold/elm-nonempty-list": "4.2.0", + "noahzgordon/elm-color-extra": "1.0.2", + "rtfeldman/elm-iso8601-date-strings": "1.1.4", + "ryan-haskell/date-format": "1.0.0", + "stoeffel/set-extra": "1.2.3", + "wernerdegroot/listzipper": "4.0.0" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/file": "1.0.5", + "elm/random": "1.0.0", + "fredcy/elm-parseint": "2.0.1", + "justinmimbs/date": "4.0.1" + } + }, + "test-dependencies": { + "direct": { + "elm-explorations/test": "2.1.1" + }, + "indirect": {} + } +} \ No newline at end of file diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000..b87b8d3d --- /dev/null +++ b/netlify.toml @@ -0,0 +1,4 @@ +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..3cd4fa82 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14590 @@ +{ + "name": "unison-share-ui", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "unison-share-ui", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "@sentry/browser": "^7.26.0", + "@sentry/tracing": "^7.26.0", + "plausible-tracker": "^0.3.8" + }, + "devDependencies": { + "@octokit/core": "^4.0.5", + "@playwright/test": "^1.39.0", + "@types/node": "^20.9.0", + "@unison-lang/ui-core-scripts": "^1.1.9", + "archiver": "^5.3.0", + "copy-webpack-plugin": "^8.1.1", + "css-loader": "^5.2.4", + "elm": "^0.19.1-5", + "elm-asset-webpack-loader": "^1.1.2", + "elm-format": "^0.8.7", + "elm-git-install": "^0.1.4", + "elm-json": "^0.2.12", + "elm-review": "^2.5.5", + "elm-test": "^0.19.1-revision12", + "elm-webpack-loader": "^8.0.0", + "favicons": "^7.0.1", + "favicons-webpack-plugin": "^6.0.0-alpha.1", + "html-webpack-plugin": "^5.3.1", + "postcss": "^8.4.16", + "postcss-loader": "^7.0.1", + "postcss-preset-env": "^7.8.0", + "prettier": "^2.8.7", + "style-loader": "^2.0.0", + "webpack": "^5.66.0", + "webpack-cli": "^4.9.1", + "webpack-dev-server": "^4.7.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@avh4/elm-format-darwin-arm64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-arm64/-/elm-format-darwin-arm64-0.8.7-2.tgz", + "integrity": "sha512-F5JD44mJ3KX960J5GkXMfh1/dtkXuPcQpX2EToHQKjLTZUfnhZ++ytQQt0gAvrJ0bzoOvhNzjNjUHDA1ruTVbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@avh4/elm-format-darwin-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-x64/-/elm-format-darwin-x64-0.8.7-2.tgz", + "integrity": "sha512-4pfF1cl0KyTion+7Mg4XKM3yi4Yc7vP76Kt/DotLVGJOSag4ISGic1og2mt8RZZ7XArybBmHNyYkiUbe/cEiCw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@avh4/elm-format-linux-arm64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-arm64/-/elm-format-linux-arm64-0.8.7-2.tgz", + "integrity": "sha512-WkVmuce2zU6s9dupHhqPc886Vaqpea8dZlxv2fpZ4wSzPUbiiKHoHZzoVndMIMTUL0TZukP3Ps0n/lWO5R5+FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@avh4/elm-format-linux-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-x64/-/elm-format-linux-x64-0.8.7-2.tgz", + "integrity": "sha512-kmncfJrTBjVT94JtQvMf4M5Pn2Yl0sZt3wo7AzgFiDnB/CiZ+KjJyXuWM64NeGiv4MQqzPq65tsFXUH1CIJeiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@avh4/elm-format-win32-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-win32-x64/-/elm-format-win32-x64-0.8.7-2.tgz", + "integrity": "sha512-sBdMBGq/8mD8Y5C+fIr5vlb3N50yB7S1MfgeAq2QEbvkr/sKrCZI540i43lZDH9gWsfA1w2W8wCe0penFYzsGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz", + "integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==", + "dev": true, + "dependencies": { + "@octokit/types": "^9.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz", + "integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz", + "integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==", + "dev": true, + "dependencies": { + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/graphql": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz", + "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==", + "dev": true, + "dependencies": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^9.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz", + "integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA==", + "dev": true + }, + "node_modules/@octokit/request": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz", + "integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^9.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/types": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz", + "integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^16.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "dev": true, + "dependencies": { + "playwright": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.45.0.tgz", + "integrity": "sha512-0aIDY2OvUX7k2XHaimOlWkboXoQvJ9dEKvfpu0Wh0YxfUTGPa+wplUdg3WVdkk018sq1L11MKmj4MPZyYUvXhw==", + "dependencies": { + "@sentry/core": "7.45.0", + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.45.0.tgz", + "integrity": "sha512-/dUrUwnI34voMj+jSJT7b5Jun+xy1utVyzzwTq3Oc22N+SB17ZOX9svZ4jl1Lu6tVJPVjPyvL6zlcbrbMwqFjg==", + "dependencies": { + "@sentry-internal/tracing": "7.45.0", + "@sentry/core": "7.45.0", + "@sentry/replay": "7.45.0", + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.45.0.tgz", + "integrity": "sha512-xJfdTS4lRmHvZI/A5MazdnKhBJFkisKu6G9EGNLlZLre+6W4PH5sb7QX4+xoBdqG7v10Jvdia112vi762ojO2w==", + "dependencies": { + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/replay": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.45.0.tgz", + "integrity": "sha512-smM7FIcFIyKu30BqCl8BzLo1gH/z9WwXdGX6V0fNvHab9fJZ09+xjFn+LmIyo6N8H8jjwsup0+yQ12kiF/ZsEw==", + "dependencies": { + "@sentry/core": "7.45.0", + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry/tracing": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.45.0.tgz", + "integrity": "sha512-FsoFmZPzTBGvWeJH73NxSF1ot61Zw3aIZo5XolengiKnRmcrQOFxebtMKBiZ61QBRYGqsm5uT7QB7zITU6Ikgg==", + "dependencies": { + "@sentry-internal/tracing": "7.45.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.45.0.tgz", + "integrity": "sha512-iFt7msfUK8LCodFF3RKUyaxy9tJv/gpWhzxUFyNxtuVwlpmd+q6mtsFGn8Af3pbpm8A+MKyz1ebMwXj0PQqknw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.45.0.tgz", + "integrity": "sha512-aTY7qqtNUudd09SH5DVSKMm3iQ6ZeWufduc0I9bPZe6UMM09BDc4KmjmrzRkdQ+VaOmHo7+v+HZKQk5f+AbuTQ==", + "dependencies": { + "@sentry/types": "7.45.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.3.tgz", + "integrity": "sha512-fa7GkppZVEByMWGbTtE5MbmXWJTVbrjjaS8K6uQj+XtuuUv1fsuPAxhygfqLmsb/Ufb3CV8deFCpiMfAgi00Sw==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", + "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@unison-lang/ui-core-scripts": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@unison-lang/ui-core-scripts/-/ui-core-scripts-1.1.9.tgz", + "integrity": "sha512-g/nZjlRN8tIcks7tUWZmNMe9bABA/jrNeIOAga4dPqarceSHgPbxl85D0LEU7zAhvPe0D2an3GPh5KKxn7apag==", + "dev": true, + "bin": { + "ui-core-check-css": "check-css-vars.mjs", + "ui-core-install": "ui-core-install.js", + "ui-core-update": "ui-core-update.js" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "dev": true, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "dev": true, + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true, + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/binwrap": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/binwrap/-/binwrap-0.2.3.tgz", + "integrity": "sha512-N4Pm7iyDEv0BrAMs+dny8WQa+e0nNTdzn2ODkf/MM6XBtKSCxCSUA1ZOQGoc1n7mUqdgOS5pwjsW91rmXVxy2Q==", + "dev": true, + "dependencies": { + "request": "^2.88.0", + "tar": "^6.1.0", + "unzip-stream": "^0.3.1" + }, + "bin": { + "binwrap-install": "bin/binwrap-install", + "binwrap-prepare": "bin/binwrap-prepare", + "binwrap-test": "bin/binwrap-test" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camel-case/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001472", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001472.tgz", + "integrity": "sha512-xWC/0+hHHQgj3/vrKYY0AAzeIUgr7L9wlELIcAvZdDUHlhL/kNxMdnQLOSOQfP8R51ZzPhmHdyMkI0MMpmxCfg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-webpack-plugin": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz", + "integrity": "sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.5", + "glob-parent": "^5.1.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dev": true, + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.5.2.tgz", + "integrity": "sha512-Xpu7Bf5Vlw+G7ikA2Lg/lVCRTSY8D5M5qFUgGNFyS4pa8ufGLyCBxIX/3if3krHlF1SKSfVPI/YsAWLDVEbocw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz", + "integrity": "sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-case/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.342", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.342.tgz", + "integrity": "sha512-dTei3VResi5bINDENswBxhL+N0Mw5YnfWyTqO75KGsVldurEkhC9+CelJVAse8jycWyP8pv3VSj4BSyP8wTWJA==", + "dev": true + }, + "node_modules/elm": { + "version": "0.19.1-5", + "resolved": "https://registry.npmjs.org/elm/-/elm-0.19.1-5.tgz", + "integrity": "sha512-dyBoPvFiNLvxOStQJdyq28gZEjS/enZXdZ5yyCtNtDEMbFJJVQq4pYNRKvhrKKdlxNot6d96iQe1uczoqO5yvA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "request": "^2.88.0" + }, + "bin": { + "elm": "bin/elm" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/elm-asset-webpack-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/elm-asset-webpack-loader/-/elm-asset-webpack-loader-1.1.3.tgz", + "integrity": "sha512-PED3ISseHh3ZKnMir2t9L5HOZOJFcodplKgSPIR+tDN9YzSRXbmyZ8E5qFehykqrmsg1H7zj1wL1/0J90judgQ==", + "dev": true + }, + "node_modules/elm-format": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/elm-format/-/elm-format-0.8.7.tgz", + "integrity": "sha512-sVzFXfWnb+6rzXK+q3e3Ccgr6/uS5mFbFk1VSmigC+x2XZ28QycAa7lS8owl009ALPhRQk+pZ95Eq5ANjpEZsQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "elm-format": "bin/elm-format" + }, + "optionalDependencies": { + "@avh4/elm-format-darwin-arm64": "0.8.7-2", + "@avh4/elm-format-darwin-x64": "0.8.7-2", + "@avh4/elm-format-linux-arm64": "0.8.7-2", + "@avh4/elm-format-linux-x64": "0.8.7-2", + "@avh4/elm-format-win32-x64": "0.8.7-2" + } + }, + "node_modules/elm-git-install": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/elm-git-install/-/elm-git-install-0.1.4.tgz", + "integrity": "sha512-XQ0Jl0RruUpcB4thmX0wnTPhB3fup9klyLmWyYOj0oUcpGlNXWr65cNbEMypxguL7sljU4MRMpItQMxeczgGwg==", + "dev": true, + "dependencies": { + "git-clone-able": "^0.1.2", + "semver": "^7.3.2", + "simple-git": "^3.3.0", + "upath": "^2.0.0" + }, + "bin": { + "elm-git-install": "index.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/elm-json": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/elm-json/-/elm-json-0.2.13.tgz", + "integrity": "sha512-KpmZIcWJbnGsUn4X1/OqGdPMWnV0kgtrK5thACwfnKHlOg3A2jMyMBWzBOJcycCpQVBC7XTVssClZGetsvaMBQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "binwrap": "^0.2.3" + }, + "bin": { + "elm-json": "bin/elm-json" + } + }, + "node_modules/elm-review": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/elm-review/-/elm-review-2.9.2.tgz", + "integrity": "sha512-fgmLh2dQnV/dtq+aAKThUwmW/MVYgkShJmvr2G3IbQJBdwJM1MzuMhIZ7S1yp1u0npN8VUn05oOoRh+utMRQXw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "chokidar": "^3.5.2", + "cross-spawn": "^7.0.3", + "elm-tooling": "^1.6.0", + "fast-levenshtein": "^3.0.0", + "find-up": "^4.1.0", + "folder-hash": "^3.3.0", + "fs-extra": "^9.0.0", + "glob": "^7.1.4", + "got": "^11.8.5", + "minimist": "^1.2.6", + "ora": "^5.4.0", + "path-key": "^3.1.1", + "prompts": "^2.2.1", + "strip-ansi": "^6.0.0", + "temp": "^0.9.1", + "terminal-link": "^2.1.1", + "which": "^2.0.2", + "wrap-ansi": "^6.2.0" + }, + "bin": { + "elm-review": "bin/elm-review" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/jfmengels" + } + }, + "node_modules/elm-solve-deps-wasm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/elm-solve-deps-wasm/-/elm-solve-deps-wasm-1.0.2.tgz", + "integrity": "sha512-qnwo7RO9IO7jd9SLHvIy0rSOEIlc/tNMTE9Cras0kl+b161PVidW4FvXo0MtXU8GAKi/2s/HYvhcnpR/NNQ1zw==", + "dev": true + }, + "node_modules/elm-test": { + "version": "0.19.1-revision12", + "resolved": "https://registry.npmjs.org/elm-test/-/elm-test-0.19.1-revision12.tgz", + "integrity": "sha512-5GV3WkJ8R/faOP1hwElQdNuCt8tKx2+1lsMrdeIYWSFz01Kp9gJl/R6zGtp4QUyrUtO8KnHsxjHrQNUf2CHkrg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "commander": "^9.4.1", + "cross-spawn": "^7.0.3", + "elm-solve-deps-wasm": "^1.0.2", + "glob": "^8.0.3", + "graceful-fs": "^4.2.10", + "split": "^1.0.1", + "which": "^2.0.2", + "xmlbuilder": "^15.1.1" + }, + "bin": { + "elm-test": "bin/elm-test" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/elm-test/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/elm-test/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/elm-test/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/elm-tooling": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.13.1.tgz", + "integrity": "sha512-a6rL9wW12Ep2oCvQtARaRpQSPGyHEoaxak6cBFej7LiKvqBgD2WrPpABNuTRP4eI3Clnmi7j2G5Nljh41+Wshg==", + "dev": true, + "bin": { + "elm-tooling": "index.js" + } + }, + "node_modules/elm-webpack-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/elm-webpack-loader/-/elm-webpack-loader-8.0.0.tgz", + "integrity": "sha512-R49j9GOGbZ+PjrktAzYzjUgf6w+RHOIUNWLfOVdc7OoGG52SreEk63l/bLJV31HwLE8PU6KANEnzeZ3238fAUg==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "node-elm-compiler": "^5.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "elm": "^0.19.1-3 || 0.19.0-no-deps" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/favicons": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/favicons/-/favicons-7.1.1.tgz", + "integrity": "sha512-+BfSVBREVYFOH8swAfhzgVEMq0vHN5tXs3f1SycPjllKiXavTnA3OAN4AJcbN2c4wdrLRzjlI6PTyBGoruHV3g==", + "dev": true, + "dependencies": { + "escape-html": "^1.0.3", + "sharp": "^0.31.1", + "xml2js": "^0.4.23" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/favicons-webpack-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/favicons-webpack-plugin/-/favicons-webpack-plugin-6.0.0.tgz", + "integrity": "sha512-wryICW2NjR9BORYjP1PN3CDbjVzXDxcemLMWQBdLJGhZlj0sYF7NNq+ddQtO/YJvBurYQ3YR1df5uXZRmcF9hw==", + "dev": true, + "dependencies": { + "find-root": "^1.1.0", + "parse-author": "^2.0.0", + "parse5": "^7.1.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "html-webpack-plugin": "^5.5.0" + }, + "peerDependencies": { + "favicons": "^7.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-elm-dependencies": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.4.tgz", + "integrity": "sha512-x/4w4fVmlD2X4PD9oQ+yh9EyaQef6OtEULdMGBTuWx0Nkppvo2Z/bAiQioW2n+GdRYKypME2b9OmYTw5tw5qDg==", + "dev": true, + "dependencies": { + "firstline": "^1.2.0", + "lodash": "^4.17.19" + }, + "bin": { + "find-elm-dependencies": "bin/cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firstline": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/firstline/-/firstline-1.3.1.tgz", + "integrity": "sha512-ycwgqtoxujz1dm0kjkBFOPQMESxB9uKc/PlD951dQDIG+tBXRpYZC2UmJb0gDxopQ1ZX6oyRQN3goRczYu7Deg==", + "dev": true, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/folder-hash": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.3.tgz", + "integrity": "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "graceful-fs": "~4.2.0", + "minimatch": "~3.0.4" + }, + "bin": { + "folder-hash": "bin/folder-hash" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/git-clone-able": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/git-clone-able/-/git-clone-able-0.1.2.tgz", + "integrity": "sha512-0pcXixfRCfLXdkwC/FJxiYEg5sYnbqYqtMmtXRzlKrStI9tLev7G/PDuFH2GmySJQ3ix5YUPRN/OJEuFD827EA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lower-case/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", + "integrity": "sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", + "integrity": "sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/no-case/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/node-abi": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.33.0.tgz", + "integrity": "sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true + }, + "node_modules/node-elm-compiler": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.6.tgz", + "integrity": "sha512-DWTRQR8b54rvschcZRREdsz7K84lnS8A6YJu8du3QLQ8f204SJbyTaA6NzYYbfUG97OTRKRv/0KZl82cTfpLhA==", + "dev": true, + "dependencies": { + "cross-spawn": "6.0.5", + "find-elm-dependencies": "^2.0.4", + "lodash": "^4.17.19", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/node-elm-compiler/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/node-elm-compiler/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/node-elm-compiler/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/node-elm-compiler/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-elm-compiler/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-elm-compiler/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plausible-tracker": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz", + "integrity": "sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "dev": true, + "dependencies": { + "playwright-core": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.1.0.tgz", + "integrity": "sha512-vTD2DJ8vJD0Vr1WzMQkRZWRjcynGh3t7NeoLg+Sb1TeuK7etiZfL/ZwHbaVa3M+Qni7Lj/29voV9IggnIUjlIw==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.0.0", + "klona": "^2.0.6", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "dev": true, + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "dev": true, + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.2.tgz", + "integrity": "sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sharp": { + "version": "0.31.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz", + "integrity": "sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.1", + "semver": "^7.3.8", + "simple-get": "^4.0.1", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", + "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-git": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.17.0.tgz", + "integrity": "sha512-JozI/s8jr3nvLd9yn2jzPVHnhVzt7t7QWfcIoDcqRIGN+f1IINGv52xoZti2kkYfoRhhRvzMSNPfogHMp97rlw==", + "dev": true, + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.16.8", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz", + "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzip-stream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", + "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", + "dev": true, + "dependencies": { + "binary": "^0.3.0", + "mkdirp": "^0.5.1" + } + }, + "node_modules/unzip-stream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/webpack": { + "version": "5.76.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.3.tgz", + "integrity": "sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", + "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.2.0", + "@webpack-cli/info": "^1.5.0", + "@webpack-cli/serve": "^1.7.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "cross-spawn": "^7.0.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "@webpack-cli/migrate": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.1.tgz", + "integrity": "sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "dev": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + }, + "dependencies": { + "@avh4/elm-format-darwin-arm64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-arm64/-/elm-format-darwin-arm64-0.8.7-2.tgz", + "integrity": "sha512-F5JD44mJ3KX960J5GkXMfh1/dtkXuPcQpX2EToHQKjLTZUfnhZ++ytQQt0gAvrJ0bzoOvhNzjNjUHDA1ruTVbg==", + "dev": true, + "optional": true + }, + "@avh4/elm-format-darwin-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-x64/-/elm-format-darwin-x64-0.8.7-2.tgz", + "integrity": "sha512-4pfF1cl0KyTion+7Mg4XKM3yi4Yc7vP76Kt/DotLVGJOSag4ISGic1og2mt8RZZ7XArybBmHNyYkiUbe/cEiCw==", + "dev": true, + "optional": true + }, + "@avh4/elm-format-linux-arm64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-arm64/-/elm-format-linux-arm64-0.8.7-2.tgz", + "integrity": "sha512-WkVmuce2zU6s9dupHhqPc886Vaqpea8dZlxv2fpZ4wSzPUbiiKHoHZzoVndMIMTUL0TZukP3Ps0n/lWO5R5+FA==", + "dev": true, + "optional": true + }, + "@avh4/elm-format-linux-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-x64/-/elm-format-linux-x64-0.8.7-2.tgz", + "integrity": "sha512-kmncfJrTBjVT94JtQvMf4M5Pn2Yl0sZt3wo7AzgFiDnB/CiZ+KjJyXuWM64NeGiv4MQqzPq65tsFXUH1CIJeiQ==", + "dev": true, + "optional": true + }, + "@avh4/elm-format-win32-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-win32-x64/-/elm-format-win32-x64-0.8.7-2.tgz", + "integrity": "sha512-sBdMBGq/8mD8Y5C+fIr5vlb3N50yB7S1MfgeAq2QEbvkr/sKrCZI540i43lZDH9gWsfA1w2W8wCe0penFYzsGw==", + "dev": true, + "optional": true + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dev": true, + "requires": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dev": true, + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "dev": true, + "requires": {} + }, + "@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "dev": true, + "requires": {} + }, + "@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "requires": { + "debug": "^4.1.1" + } + }, + "@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@octokit/auth-token": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.3.tgz", + "integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==", + "dev": true, + "requires": { + "@octokit/types": "^9.0.0" + } + }, + "@octokit/core": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz", + "integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==", + "dev": true, + "requires": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.5.tgz", + "integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==", + "dev": true, + "requires": { + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.5.tgz", + "integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==", + "dev": true, + "requires": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^9.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-16.0.0.tgz", + "integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA==", + "dev": true + }, + "@octokit/request": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.3.tgz", + "integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==", + "dev": true, + "requires": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "dev": true, + "requires": { + "@octokit/types": "^9.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/types": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.0.0.tgz", + "integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==", + "dev": true, + "requires": { + "@octokit/openapi-types": "^16.0.0" + } + }, + "@playwright/test": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "dev": true, + "requires": { + "playwright": "1.39.0" + } + }, + "@sentry-internal/tracing": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.45.0.tgz", + "integrity": "sha512-0aIDY2OvUX7k2XHaimOlWkboXoQvJ9dEKvfpu0Wh0YxfUTGPa+wplUdg3WVdkk018sq1L11MKmj4MPZyYUvXhw==", + "requires": { + "@sentry/core": "7.45.0", + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0", + "tslib": "^1.9.3" + } + }, + "@sentry/browser": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.45.0.tgz", + "integrity": "sha512-/dUrUwnI34voMj+jSJT7b5Jun+xy1utVyzzwTq3Oc22N+SB17ZOX9svZ4jl1Lu6tVJPVjPyvL6zlcbrbMwqFjg==", + "requires": { + "@sentry-internal/tracing": "7.45.0", + "@sentry/core": "7.45.0", + "@sentry/replay": "7.45.0", + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0", + "tslib": "^1.9.3" + } + }, + "@sentry/core": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.45.0.tgz", + "integrity": "sha512-xJfdTS4lRmHvZI/A5MazdnKhBJFkisKu6G9EGNLlZLre+6W4PH5sb7QX4+xoBdqG7v10Jvdia112vi762ojO2w==", + "requires": { + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0", + "tslib": "^1.9.3" + } + }, + "@sentry/replay": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.45.0.tgz", + "integrity": "sha512-smM7FIcFIyKu30BqCl8BzLo1gH/z9WwXdGX6V0fNvHab9fJZ09+xjFn+LmIyo6N8H8jjwsup0+yQ12kiF/ZsEw==", + "requires": { + "@sentry/core": "7.45.0", + "@sentry/types": "7.45.0", + "@sentry/utils": "7.45.0" + } + }, + "@sentry/tracing": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.45.0.tgz", + "integrity": "sha512-FsoFmZPzTBGvWeJH73NxSF1ot61Zw3aIZo5XolengiKnRmcrQOFxebtMKBiZ61QBRYGqsm5uT7QB7zITU6Ikgg==", + "requires": { + "@sentry-internal/tracing": "7.45.0" + } + }, + "@sentry/types": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.45.0.tgz", + "integrity": "sha512-iFt7msfUK8LCodFF3RKUyaxy9tJv/gpWhzxUFyNxtuVwlpmd+q6mtsFGn8Af3pbpm8A+MKyz1ebMwXj0PQqknw==" + }, + "@sentry/utils": { + "version": "7.45.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.45.0.tgz", + "integrity": "sha512-aTY7qqtNUudd09SH5DVSKMm3iQ6ZeWufduc0I9bPZe6UMM09BDc4KmjmrzRkdQ+VaOmHo7+v+HZKQk5f+AbuTQ==", + "requires": { + "@sentry/types": "7.45.0", + "tslib": "^1.9.3" + } + }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.3.tgz", + "integrity": "sha512-fa7GkppZVEByMWGbTtE5MbmXWJTVbrjjaS8K6uQj+XtuuUv1fsuPAxhygfqLmsb/Ufb3CV8deFCpiMfAgi00Sw==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "@types/http-cache-semantics": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", + "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "dev": true + }, + "@types/http-proxy": { + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", + "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "@types/node": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@unison-lang/ui-core-scripts": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@unison-lang/ui-core-scripts/-/ui-core-scripts-1.1.9.tgz", + "integrity": "sha512-g/nZjlRN8tIcks7tUWZmNMe9bABA/jrNeIOAga4dPqarceSHgPbxl85D0LEU7zAhvPe0D2an3GPh5KKxn7apag==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "dev": true, + "requires": {} + }, + "@webpack-cli/info": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "dev": true, + "requires": { + "envinfo": "^7.7.3" + } + }, + "@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true, + "requires": {} + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "dev": true, + "requires": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + } + }, + "archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "requires": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "requires": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true + }, + "aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "binwrap": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/binwrap/-/binwrap-0.2.3.tgz", + "integrity": "sha512-N4Pm7iyDEv0BrAMs+dny8WQa+e0nNTdzn2ODkf/MM6XBtKSCxCSUA1ZOQGoc1n7mUqdgOS5pwjsW91rmXVxy2Q==", + "dev": true, + "requires": { + "request": "^2.88.0", + "tar": "^6.1.0", + "unzip-stream": "^0.3.1" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "requires": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true + }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30001472", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001472.tgz", + "integrity": "sha512-xWC/0+hHHQgj3/vrKYY0AAzeIUgr7L9wlELIcAvZdDUHlhL/kNxMdnQLOSOQfP8R51ZzPhmHdyMkI0MMpmxCfg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "dev": true + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + }, + "compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "dev": true, + "requires": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "copy-webpack-plugin": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz", + "integrity": "sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ==", + "dev": true, + "requires": { + "fast-glob": "^3.2.5", + "glob-parent": "^5.1.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "p-limit": "^3.1.0", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "cosmiconfig": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", + "dev": true, + "requires": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + } + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true + }, + "crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "dev": true, + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-loader": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", + "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.5" + } + }, + "css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "requires": {} + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "cssdb": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.5.2.tgz", + "integrity": "sha512-Xpu7Bf5Vlw+G7ikA2Lg/lVCRTSY8D5M5qFUgGNFyS4pa8ufGLyCBxIX/3if3krHlF1SKSfVPI/YsAWLDVEbocw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + } + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "dns-packet": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz", + "integrity": "sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA==", + "dev": true, + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "dependencies": { + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + } + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.342", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.342.tgz", + "integrity": "sha512-dTei3VResi5bINDENswBxhL+N0Mw5YnfWyTqO75KGsVldurEkhC9+CelJVAse8jycWyP8pv3VSj4BSyP8wTWJA==", + "dev": true + }, + "elm": { + "version": "0.19.1-5", + "resolved": "https://registry.npmjs.org/elm/-/elm-0.19.1-5.tgz", + "integrity": "sha512-dyBoPvFiNLvxOStQJdyq28gZEjS/enZXdZ5yyCtNtDEMbFJJVQq4pYNRKvhrKKdlxNot6d96iQe1uczoqO5yvA==", + "dev": true, + "requires": { + "request": "^2.88.0" + } + }, + "elm-asset-webpack-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/elm-asset-webpack-loader/-/elm-asset-webpack-loader-1.1.3.tgz", + "integrity": "sha512-PED3ISseHh3ZKnMir2t9L5HOZOJFcodplKgSPIR+tDN9YzSRXbmyZ8E5qFehykqrmsg1H7zj1wL1/0J90judgQ==", + "dev": true + }, + "elm-format": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/elm-format/-/elm-format-0.8.7.tgz", + "integrity": "sha512-sVzFXfWnb+6rzXK+q3e3Ccgr6/uS5mFbFk1VSmigC+x2XZ28QycAa7lS8owl009ALPhRQk+pZ95Eq5ANjpEZsQ==", + "dev": true, + "requires": { + "@avh4/elm-format-darwin-arm64": "0.8.7-2", + "@avh4/elm-format-darwin-x64": "0.8.7-2", + "@avh4/elm-format-linux-arm64": "0.8.7-2", + "@avh4/elm-format-linux-x64": "0.8.7-2", + "@avh4/elm-format-win32-x64": "0.8.7-2" + } + }, + "elm-git-install": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/elm-git-install/-/elm-git-install-0.1.4.tgz", + "integrity": "sha512-XQ0Jl0RruUpcB4thmX0wnTPhB3fup9klyLmWyYOj0oUcpGlNXWr65cNbEMypxguL7sljU4MRMpItQMxeczgGwg==", + "dev": true, + "requires": { + "git-clone-able": "^0.1.2", + "semver": "^7.3.2", + "simple-git": "^3.3.0", + "upath": "^2.0.0" + } + }, + "elm-json": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/elm-json/-/elm-json-0.2.13.tgz", + "integrity": "sha512-KpmZIcWJbnGsUn4X1/OqGdPMWnV0kgtrK5thACwfnKHlOg3A2jMyMBWzBOJcycCpQVBC7XTVssClZGetsvaMBQ==", + "dev": true, + "requires": { + "binwrap": "^0.2.3" + } + }, + "elm-review": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/elm-review/-/elm-review-2.9.2.tgz", + "integrity": "sha512-fgmLh2dQnV/dtq+aAKThUwmW/MVYgkShJmvr2G3IbQJBdwJM1MzuMhIZ7S1yp1u0npN8VUn05oOoRh+utMRQXw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "chokidar": "^3.5.2", + "cross-spawn": "^7.0.3", + "elm-tooling": "^1.6.0", + "fast-levenshtein": "^3.0.0", + "find-up": "^4.1.0", + "folder-hash": "^3.3.0", + "fs-extra": "^9.0.0", + "glob": "^7.1.4", + "got": "^11.8.5", + "minimist": "^1.2.6", + "ora": "^5.4.0", + "path-key": "^3.1.1", + "prompts": "^2.2.1", + "strip-ansi": "^6.0.0", + "temp": "^0.9.1", + "terminal-link": "^2.1.1", + "which": "^2.0.2", + "wrap-ansi": "^6.2.0" + } + }, + "elm-solve-deps-wasm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/elm-solve-deps-wasm/-/elm-solve-deps-wasm-1.0.2.tgz", + "integrity": "sha512-qnwo7RO9IO7jd9SLHvIy0rSOEIlc/tNMTE9Cras0kl+b161PVidW4FvXo0MtXU8GAKi/2s/HYvhcnpR/NNQ1zw==", + "dev": true + }, + "elm-test": { + "version": "0.19.1-revision12", + "resolved": "https://registry.npmjs.org/elm-test/-/elm-test-0.19.1-revision12.tgz", + "integrity": "sha512-5GV3WkJ8R/faOP1hwElQdNuCt8tKx2+1lsMrdeIYWSFz01Kp9gJl/R6zGtp4QUyrUtO8KnHsxjHrQNUf2CHkrg==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "commander": "^9.4.1", + "cross-spawn": "^7.0.3", + "elm-solve-deps-wasm": "^1.0.2", + "glob": "^8.0.3", + "graceful-fs": "^4.2.10", + "split": "^1.0.1", + "which": "^2.0.2", + "xmlbuilder": "^15.1.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "elm-tooling": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/elm-tooling/-/elm-tooling-1.13.1.tgz", + "integrity": "sha512-a6rL9wW12Ep2oCvQtARaRpQSPGyHEoaxak6cBFej7LiKvqBgD2WrPpABNuTRP4eI3Clnmi7j2G5Nljh41+Wshg==", + "dev": true + }, + "elm-webpack-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/elm-webpack-loader/-/elm-webpack-loader-8.0.0.tgz", + "integrity": "sha512-R49j9GOGbZ+PjrktAzYzjUgf6w+RHOIUNWLfOVdc7OoGG52SreEk63l/bLJV31HwLE8PU6KANEnzeZ3238fAUg==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "node-elm-compiler": "^5.0.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "enhanced-resolve": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", + "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "entities": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", + "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "dev": true + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + } + } + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true + }, + "express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, + "requires": { + "fastest-levenshtein": "^1.0.7" + } + }, + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "favicons": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/favicons/-/favicons-7.1.1.tgz", + "integrity": "sha512-+BfSVBREVYFOH8swAfhzgVEMq0vHN5tXs3f1SycPjllKiXavTnA3OAN4AJcbN2c4wdrLRzjlI6PTyBGoruHV3g==", + "dev": true, + "requires": { + "escape-html": "^1.0.3", + "sharp": "^0.31.1", + "xml2js": "^0.4.23" + } + }, + "favicons-webpack-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/favicons-webpack-plugin/-/favicons-webpack-plugin-6.0.0.tgz", + "integrity": "sha512-wryICW2NjR9BORYjP1PN3CDbjVzXDxcemLMWQBdLJGhZlj0sYF7NNq+ddQtO/YJvBurYQ3YR1df5uXZRmcF9hw==", + "dev": true, + "requires": { + "find-root": "^1.1.0", + "html-webpack-plugin": "^5.5.0", + "parse-author": "^2.0.0", + "parse5": "^7.1.1" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "find-elm-dependencies": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.4.tgz", + "integrity": "sha512-x/4w4fVmlD2X4PD9oQ+yh9EyaQef6OtEULdMGBTuWx0Nkppvo2Z/bAiQioW2n+GdRYKypME2b9OmYTw5tw5qDg==", + "dev": true, + "requires": { + "firstline": "^1.2.0", + "lodash": "^4.17.19" + } + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "firstline": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/firstline/-/firstline-1.3.1.tgz", + "integrity": "sha512-ycwgqtoxujz1dm0kjkBFOPQMESxB9uKc/PlD951dQDIG+tBXRpYZC2UmJb0gDxopQ1ZX6oyRQN3goRczYu7Deg==", + "dev": true + }, + "folder-hash": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.3.tgz", + "integrity": "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "graceful-fs": "~4.2.0", + "minimatch": "~3.0.4" + } + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "git-clone-able": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/git-clone-able/-/git-clone-able-0.1.2.tgz", + "integrity": "sha512-0pcXixfRCfLXdkwC/FJxiYEg5sYnbqYqtMmtXRzlKrStI9tLev7G/PDuFH2GmySJQ3ix5YUPRN/OJEuFD827EA==", + "dev": true + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "requires": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + } + } + }, + "html-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==", + "dev": true, + "requires": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + } + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + }, + "dependencies": { + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + } + } + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "requires": {} + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true + }, + "ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "keyv": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", + "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true + }, + "launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "requires": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true + }, + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true + }, + "memfs": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.13.tgz", + "integrity": "sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==", + "dev": true, + "requires": { + "fs-monkey": "^1.0.3" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "minipass": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", + "integrity": "sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==", + "dev": true + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, + "node-abi": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.33.0.tgz", + "integrity": "sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true + }, + "node-elm-compiler": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.6.tgz", + "integrity": "sha512-DWTRQR8b54rvschcZRREdsz7K84lnS8A6YJu8du3QLQ8f204SJbyTaA6NzYYbfUG97OTRKRv/0KZl82cTfpLhA==", + "dev": true, + "requires": { + "cross-spawn": "6.0.5", + "find-elm-dependencies": "^2.0.4", + "lodash": "^4.17.19", + "temp": "^0.9.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true + }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "requires": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "requires": { + "author-regex": "^1.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", + "dev": true + } + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "plausible-tracker": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz", + "integrity": "sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==" + }, + "playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.39.0" + } + }, + "playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true + }, + "postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "requires": {} + }, + "postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "dev": true, + "requires": {} + }, + "postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "requires": {} + }, + "postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-loader": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.1.0.tgz", + "integrity": "sha512-vTD2DJ8vJD0Vr1WzMQkRZWRjcynGh3t7NeoLg+Sb1TeuK7etiZfL/ZwHbaVa3M+Qni7Lj/29voV9IggnIUjlIw==", + "dev": true, + "requires": { + "cosmiconfig": "^8.0.0", + "klona": "^2.0.6", + "semver": "^7.3.8" + } + }, + "postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "requires": {} + }, + "postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "requires": {} + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dev": true, + "requires": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "dev": true, + "requires": {} + }, + "postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "requires": {} + }, + "postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "dev": true, + "requires": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "requires": {} + }, + "postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.10" + } + }, + "postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "prettier": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", + "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "dev": true + }, + "pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "requires": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + } + } + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.2.tgz", + "integrity": "sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA==", + "dev": true, + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "requires": { + "resolve": "^1.9.0" + } + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true + }, + "renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "requires": { + "node-forge": "^1" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "sharp": { + "version": "0.31.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.3.tgz", + "integrity": "sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==", + "dev": true, + "requires": { + "color": "^4.2.3", + "detect-libc": "^2.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.1", + "semver": "^7.3.8", + "simple-get": "^4.0.1", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shell-quote": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz", + "integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "simple-git": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.17.0.tgz", + "integrity": "sha512-JozI/s8jr3nvLd9yn2jzPVHnhVzt7t7QWfcIoDcqRIGN+f1IINGv52xoZti2kkYfoRhhRvzMSNPfogHMp97rlw==", + "dev": true, + "requires": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.4" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + } + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + }, + "style-loader": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz", + "integrity": "sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + } + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "terser": { + "version": "5.16.8", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz", + "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + }, + "dependencies": { + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + } + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true + }, + "unzip-stream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", + "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", + "dev": true, + "requires": { + "binary": "^0.3.0", + "mkdirp": "^0.5.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + } + } + }, + "upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "webpack": { + "version": "5.76.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.3.tgz", + "integrity": "sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + } + }, + "webpack-cli": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", + "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.2.0", + "@webpack-cli/info": "^1.5.0", + "@webpack-cli/serve": "^1.7.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "cross-spawn": "^7.0.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + } + } + }, + "webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "webpack-dev-server": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.1.tgz", + "integrity": "sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA==", + "dev": true, + "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "requires": {} + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "dependencies": { + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + } + } + }, + "xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "dev": true, + "requires": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..ed2842cf --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "unison-share-ui", + "version": "1.0.0", + "description": "Unison Share UI", + "repository": { + "type": "git", + "url": "git+https://github.com/unisoncomputing/unison-share-ui.git" + }, + "scripts": { + "build": "webpack --mode production --config webpack.prod.js", + "clean": "rm -rf dist", + "start": "webpack serve --mode development --port 1234 --config webpack.dev.js", + "test": "elm-test", + "check": "elm-test; elm-review; ui-core-check-css; npx prettier --check .", + "watch": "elm-watch hot", + "review": "elm-review", + "build-maintenance-mode": "rm -rf dist; mkdir -p dist/unisonShare; cp src/maintenance.html dist/unisonShare/index.html", + "repl": "elm repl", + "ui-core-check-css": "ui-core-check-css", + "ui-core-install": "ui-core-install", + "ui-core-update": "ui-core-update", + "postinstall": "ui-core-install" + }, + "homepage": "https://share.unison-lang.org", + "devDependencies": { + "@octokit/core": "^4.0.5", + "@playwright/test": "^1.39.0", + "@types/node": "^20.9.0", + "@unison-lang/ui-core-scripts": "^1.1.9", + "archiver": "^5.3.0", + "copy-webpack-plugin": "^8.1.1", + "css-loader": "^5.2.4", + "elm": "^0.19.1-5", + "elm-asset-webpack-loader": "^1.1.2", + "elm-format": "^0.8.7", + "elm-git-install": "^0.1.4", + "elm-json": "^0.2.12", + "elm-review": "^2.5.5", + "elm-test": "^0.19.1-revision12", + "elm-webpack-loader": "^8.0.0", + "favicons": "^7.0.1", + "favicons-webpack-plugin": "^6.0.0-alpha.1", + "html-webpack-plugin": "^5.3.1", + "postcss": "^8.4.16", + "postcss-loader": "^7.0.1", + "postcss-preset-env": "^7.8.0", + "prettier": "^2.8.7", + "style-loader": "^2.0.0", + "webpack": "^5.66.0", + "webpack-cli": "^4.9.1", + "webpack-dev-server": "^4.7.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "@sentry/browser": "^7.26.0", + "@sentry/tracing": "^7.26.0", + "plausible-tracker": "^0.3.8" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..38d8d2ee --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests/e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "list", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + webServer: { + command: "npm run start", + url: "http://127.0.0.1:1234", + reuseExistingServer: !process.env.CI, + stdout: "ignore", + stderr: "pipe", + }, +}); diff --git a/review/elm.json b/review/elm.json new file mode 100644 index 00000000..9afc6d75 --- /dev/null +++ b/review/elm.json @@ -0,0 +1,38 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.5", + "jfmengels/elm-review": "2.12.2", + "jfmengels/elm-review-simplify": "2.0.28", + "jfmengels/elm-review-unused": "1.1.29", + "stil4m/elm-syntax": "7.2.9" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/html": "1.0.0", + "elm/json": "1.1.3", + "elm/parser": "1.1.0", + "elm/project-metadata-utils": "1.0.2", + "elm/random": "1.0.0", + "elm/time": "1.0.0", + "elm/virtual-dom": "1.0.3", + "elm-community/list-extra": "8.7.0", + "elm-explorations/test": "2.1.1", + "miniBill/elm-unicode": "1.0.3", + "pzp1997/assoc-list": "1.0.0", + "rtfeldman/elm-hex": "1.0.0", + "stil4m/structured-writer": "1.0.3" + } + }, + "test-dependencies": { + "direct": { + "elm-explorations/test": "2.1.1" + }, + "indirect": {} + } +} diff --git a/review/src/ReviewConfig.elm b/review/src/ReviewConfig.elm new file mode 100644 index 00000000..b504fe7d --- /dev/null +++ b/review/src/ReviewConfig.elm @@ -0,0 +1,20 @@ +module ReviewConfig exposing (config) + +import NoUnused.Dependencies +import NoUnused.Modules +import NoUnused.Parameters +import NoUnused.Patterns +import NoUnused.Variables +import Review.Rule exposing (Rule) +import Simplify + + +config : List Rule +config = + [ NoUnused.Dependencies.rule + , NoUnused.Modules.rule + , NoUnused.Parameters.rule + , NoUnused.Patterns.rule + , NoUnused.Variables.rule + , Simplify.rule Simplify.defaults + ] diff --git a/review/suppressed/NoUnused.Dependencies.json b/review/suppressed/NoUnused.Dependencies.json new file mode 100644 index 00000000..5405a038 --- /dev/null +++ b/review/suppressed/NoUnused.Dependencies.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "automatically created by": "elm-review suppress", + "learn more": "elm-review suppress --help", + "suppressions": [ + { "count": 12, "filePath": "elm.json" } + ] +} diff --git a/review/suppressed/NoUnused.Modules.json b/review/suppressed/NoUnused.Modules.json new file mode 100644 index 00000000..e727a94a --- /dev/null +++ b/review/suppressed/NoUnused.Modules.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "automatically created by": "elm-review suppress", + "learn more": "elm-review suppress --help", + "suppressions": [ + { "count": 1, "filePath": "src/UnisonShare/Log.elm" }, + { "count": 1, "filePath": "src/UnisonShare/Metrics.elm" } + ] +} diff --git a/src/404.html b/src/404.html new file mode 100644 index 00000000..948b1aee --- /dev/null +++ b/src/404.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + Unison Share | Not Found + + + + +
+

Unison Share

+

We're sorry, but we couldn't find the page you were looking for.

+
+ + + diff --git a/src/500.html b/src/500.html new file mode 100644 index 00000000..01607eaf --- /dev/null +++ b/src/500.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + Unison Share | Application Error + + + + +
+

Unison Share

+

+ We're sorry, but we encounted a problem, and couldn't start the + application. +

+
+ + + diff --git a/src/UnisonShare.elm b/src/UnisonShare.elm new file mode 100644 index 00000000..abb6913d --- /dev/null +++ b/src/UnisonShare.elm @@ -0,0 +1,18 @@ +module UnisonShare exposing (..) + +import Browser +import UnisonShare.App as App +import UnisonShare.AppContext exposing (Flags) +import UnisonShare.PreApp as PreApp + + +main : Program Flags PreApp.Model PreApp.Msg +main = + Browser.application + { init = PreApp.init + , update = PreApp.update + , view = PreApp.view + , subscriptions = PreApp.subscriptions + , onUrlRequest = App.LinkClicked >> PreApp.AppMsg + , onUrlChange = App.UrlChanged >> PreApp.AppMsg + } diff --git a/src/UnisonShare/Account.elm b/src/UnisonShare/Account.elm new file mode 100644 index 00000000..4b1ff73a --- /dev/null +++ b/src/UnisonShare/Account.elm @@ -0,0 +1,114 @@ +module UnisonShare.Account exposing (..) + +import Json.Decode as Decode exposing (field, maybe, string) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Lib.Util exposing (decodeUrl) +import UI.Avatar as Avatar exposing (Avatar) +import UI.Icon as Icon +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Tour as Tour exposing (Tour) +import UnisonShare.User exposing (UserSummary) +import Url exposing (Url) + + +type alias Account a = + { a + | handle : UserHandle + , name : Maybe String + , avatarUrl : Maybe Url + , pronouns : Maybe String + , completedTours : List Tour + , organizationMemberships : List OrganizationMembership + } + + +type OrganizationMembership + = OrganizationMembership UserHandle + + +type alias AccountSummary = + Account {} + + + +-- HELPERS + + +toUserSummary : Account a -> UserSummary +toUserSummary account = + { handle = account.handle + , name = account.name + , avatarUrl = account.avatarUrl + , pronouns = account.pronouns + } + + +name : Account a -> String +name account = + Maybe.withDefault (UserHandle.toString account.handle) account.name + + +isOrganizationMember : UserHandle -> Account a -> Bool +isOrganizationMember orgHandle account = + account.organizationMemberships + |> List.map (\(OrganizationMembership handle) -> handle) + |> List.member orgHandle + + +isUnisonMember : Account a -> Bool +isUnisonMember account = + isOrganizationMember (UserHandle.unsafeFromString "unison") account + + +toAvatar : Account a -> Avatar msg +toAvatar account = + Avatar.avatar account.avatarUrl (Just (name account)) + |> Avatar.withIcon Icon.user + + +hasCompletedTour : Tour -> Account a -> Bool +hasCompletedTour tour { completedTours } = + List.member tour completedTours + + +markTourAsCompleted : Tour -> Account a -> Account a +markTourAsCompleted tour account = + { account | completedTours = account.completedTours ++ [ tour ] } + + +isProjectOwner : ProjectRef -> Account a -> Bool +isProjectOwner projectRef account = + UserHandle.equals (ProjectRef.handle projectRef) account.handle + + +hasProjectAccess : ProjectRef -> Account a -> Bool +hasProjectAccess projectRef account = + let + projectHandle = + ProjectRef.handle projectRef + in + UserHandle.equals projectHandle account.handle || isOrganizationMember projectHandle account + + + +-- DECODE + + +decodeSummary : Decode.Decoder AccountSummary +decodeSummary = + let + makeSummary handle name_ avatarUrl completedTours organizationMemberships = + { handle = handle + , name = name_ + , avatarUrl = avatarUrl + , pronouns = Nothing + , completedTours = Maybe.withDefault [] completedTours + , organizationMemberships = organizationMemberships + } + in + Decode.map5 makeSummary + (field "handle" UserHandle.decodeUnprefixed) + (maybe (field "name" string)) + (maybe (field "avatarUrl" decodeUrl)) + (maybe (field "completedTours" (Decode.list Tour.decode))) + (field "organizationMemberships" (Decode.list (Decode.map OrganizationMembership UserHandle.decodeUnprefixed))) diff --git a/src/UnisonShare/Api.elm b/src/UnisonShare/Api.elm new file mode 100644 index 00000000..f747e2a4 --- /dev/null +++ b/src/UnisonShare/Api.elm @@ -0,0 +1,1134 @@ +module UnisonShare.Api exposing + ( NewProjectContribution + , NewProjectTicket + , ProjectBranchesKindFilter(..) + , ProjectBranchesParams + , ProjectContributionUpdate(..) + , ProjectTicketUpdate(..) + , ProjectUpdate(..) + , UserBranchesParams + , browseCodebase + , catalog + , codebaseApiEndpointToEndpoint + , completeTours + , createProjectContribution + , createProjectContributionComment + , createProjectRelease + , createProjectTicket + , createProjectTicketComment + , createSupportTicket + , deleteProject + , deleteProjectBranch + , deleteProjectContributionComment + , deleteProjectTicketComment + , namespace + , project + , projectBranch + , projectBranchDiff + , projectBranchReleaseNotes + , projectBranches + , projectContribution + , projectContributionDiff + , projectContributionTimeline + , projectContributions + , projectReadme + , projectRelease + , projectReleaseNotes + , projectReleases + , projectTicket + , projectTicketTimeline + , projectTickets + , projects + , search + , session + , updateProject + , updateProjectContribution + , updateProjectContributionComment + , updateProjectFav + , updateProjectTicket + , updateProjectTicketComment + , updateUserProfile + , user + , userBranches + , userProjects + , userReadme + ) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.CodebaseApi as CodebaseApi +import Code.Definition.Reference as Reference exposing (Reference(..)) +import Code.FullyQualifiedName as FQN exposing (FQN) +import Code.Hash as Hash exposing (Hash) +import Code.HashQualified as HQ +import Code.Namespace.NamespaceRef as NamespaceRef exposing (NamespaceRef) +import Code.Perspective as Perspective exposing (Perspective(..)) +import Code.Syntax as Syntax +import Code.Version as Version exposing (Version) +import Http +import Json.Encode as Encode +import Json.Encode.Extra as EncodeE +import Lib.HttpApi exposing (Endpoint(..)) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Maybe.Extra as MaybeE +import Regex +import Set exposing (Set) +import UI.KeyboardShortcut.Key exposing (Key(..)) +import UnisonShare.CodeBrowsingContext exposing (CodeBrowsingContext(..)) +import UnisonShare.Contribution.ContributionRef as ContributionRef exposing (ContributionRef) +import UnisonShare.Contribution.ContributionStatus as ContributionStatus exposing (ContributionStatus) +import UnisonShare.Project as Project exposing (ProjectVisibility) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Ticket.TicketRef as TicketRef exposing (TicketRef) +import UnisonShare.Ticket.TicketStatus as TicketStatus exposing (TicketStatus) +import UnisonShare.Timeline.CommentId as CommentId exposing (CommentId) +import UnisonShare.Tour as Tour exposing (Tour) +import Url.Builder exposing (QueryParameter, int, string) + + +user : UserHandle -> Endpoint +user handle = + GET { path = [ "users", UserHandle.toUnprefixedString handle ], queryParams = [] } + + +updateUserProfile : UserHandle -> { bio : String } -> Endpoint +updateUserProfile handle profile = + let + body = + Encode.object [ ( "bio", Encode.string profile.bio ) ] + |> Http.jsonBody + in + PATCH + { path = [ "users", UserHandle.toUnprefixedString handle ] + , queryParams = [] + , body = body + } + + +userReadme : UserHandle -> Endpoint +userReadme handle = + GET { path = [ "users", UserHandle.toUnprefixedString handle, "readme" ], queryParams = [] } + + +type alias UserBranchesParams = + { searchQuery : Maybe String + , projectRef : Maybe ProjectRef + , limit : Int + , cursor : Maybe String + } + + +userBranches : UserHandle -> UserBranchesParams -> Endpoint +userBranches handle params = + let + queryParams = + int "limit" params.limit + :: (params.cursor + |> Maybe.map (string "cursor") + |> MaybeE.toList + ) + ++ (params.searchQuery + |> Maybe.map (string "name-prefix") + |> MaybeE.toList + ) + ++ (params.projectRef + |> Maybe.map (ProjectRef.toString >> string "project-ref") + |> MaybeE.toList + ) + in + GET + { path = [ "users", UserHandle.toUnprefixedString handle, "branches" ] + , queryParams = queryParams + } + + +userProjects : UserHandle -> Endpoint +userProjects handle = + GET { path = [ "users", UserHandle.toUnprefixedString handle, "projects" ], queryParams = [] } + + +search : String -> Endpoint +search query = + GET { path = [ "search" ], queryParams = [ string "query" query ] } + + +completeTours : List Tour -> Endpoint +completeTours tours = + let + body = + tours + |> List.map Tour.toString + |> Encode.list Encode.string + |> Http.jsonBody + in + POST + { path = [ "account", "tours" ] + , queryParams = [] + , body = body + } + + +createSupportTicket : + { subject : String, body : String, tags : List String } + -> Endpoint +createSupportTicket data = + let + body = + Encode.object + [ ( "subject", Encode.string data.subject ) + , ( "body", Encode.string data.body ) + , ( "priority", Encode.string "normal" ) + , ( "tags", Encode.list Encode.string data.tags ) + ] + in + POST { path = [ "support", "tickets" ], queryParams = [], body = Http.jsonBody body } + + +{-| TODO: +Should likely be a /session endpoint + +Instead, for now we're using the /account endpoint, but over time that endpoint +is likely to grow to include data not needed for Session. + +-} +session : Endpoint +session = + GET { path = [ "account" ], queryParams = [] } + + + +-- PROJECTS + + +project : ProjectRef -> Endpoint +project projectRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET { path = [ "users", handle, "projects", slug ], queryParams = [] } + + +type ProjectBranchesKindFilter + = AllBranches (Maybe UserHandle) + | ContributorBranches (Maybe UserHandle) + | ProjectBranches + + +type alias ProjectBranchesParams = + { kind : ProjectBranchesKindFilter + , searchQuery : Maybe String + , limit : Int + , cursor : Maybe String + } + + +projectBranches : ProjectRef -> ProjectBranchesParams -> Endpoint +projectBranches projectRef params = + let + ( kind, contributorHandleParams ) = + case params.kind of + AllBranches Nothing -> + ( string "kind" "all", [] ) + + AllBranches (Just h) -> + ( string "kind" "all", [ h |> UserHandle.toString |> string "contributor-handle" ] ) + + ContributorBranches Nothing -> + ( string "kind" "contributor", [] ) + + ContributorBranches (Just h) -> + ( string "kind" "contributor", [ h |> UserHandle.toString |> string "contributor-handle" ] ) + + ProjectBranches -> + ( string "kind" "core", [] ) + + queryParams = + [ kind, int "limit" params.limit ] + ++ (params.cursor + |> Maybe.map (string "cursor") + |> MaybeE.toList + ) + ++ (params.searchQuery + |> Maybe.map (string "name-prefix") + |> MaybeE.toList + ) + ++ contributorHandleParams + + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "branches" ] + , queryParams = queryParams + } + + +projectBranch : ProjectRef -> BranchRef -> Endpoint +projectBranch projectRef branchRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "branches", BranchRef.toApiUrlString branchRef ] + , queryParams = [] + } + + +projectBranchReleaseNotes : ProjectRef -> BranchRef -> Endpoint +projectBranchReleaseNotes projectRef branchRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = + [ "users" + , handle + , "projects" + , slug + , "branches" + , BranchRef.toApiUrlString branchRef + , "releaseNotes" + ] + , queryParams = [] + } + + + +-- PROJECT CONTRIBUTIONS + + +projectContribution : ProjectRef -> ContributionRef -> Endpoint +projectContribution projectRef contribRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "contributions", ContributionRef.toApiString contribRef ] + , queryParams = [] + } + + +type alias NewProjectContribution = + { title : String + , description : Maybe String + , status : ContributionStatus + , sourceBranchRef : BranchRef + , targetBranchRef : BranchRef + } + + +createProjectContribution : ProjectRef -> NewProjectContribution -> Endpoint +createProjectContribution projectRef data = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object + [ ( "title", Encode.string data.title ) + , ( "description", MaybeE.unwrap Encode.null Encode.string data.description ) + , ( "status", Encode.string (ContributionStatus.toApiString data.status) ) + , ( "sourceBranchRef", Encode.string (BranchRef.toString data.sourceBranchRef) ) + , ( "targetBranchRef", Encode.string (BranchRef.toString data.targetBranchRef) ) + ] + in + POST + { path = [ "users", handle, "projects", slug, "contributions" ] + , queryParams = [] + , body = Http.jsonBody body + } + + +createProjectContributionComment : ProjectRef -> ContributionRef -> String -> Endpoint +createProjectContributionComment projectRef contribRef comment = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object + [ ( "content", Encode.string comment ) + ] + in + POST + { path = + [ "users" + , handle + , "projects" + , slug + , "contributions" + , ContributionRef.toApiString contribRef + , "timeline" + , "comments" + ] + , queryParams = [] + , body = Http.jsonBody body + } + + +updateProjectContributionComment : ProjectRef -> ContributionRef -> CommentId -> Int -> String -> Endpoint +updateProjectContributionComment projectRef contribRef commentId originalRevision comment = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object + [ ( "content", Encode.string comment ) + , ( "expectedRevision", Encode.int originalRevision ) + ] + in + PATCH + { path = + [ "users" + , handle + , "projects" + , slug + , "contributions" + , ContributionRef.toApiString contribRef + , "timeline" + , "comments" + , CommentId.toString commentId + ] + , queryParams = [] + , body = Http.jsonBody body + } + + +deleteProjectContributionComment : ProjectRef -> ContributionRef -> CommentId -> Endpoint +deleteProjectContributionComment projectRef contribRef commentId = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + DELETE + { path = + [ "users" + , handle + , "projects" + , slug + , "contributions" + , ContributionRef.toApiString contribRef + , "timeline" + , "comments" + , CommentId.toString commentId + ] + , queryParams = [] + } + + +type ProjectContributionUpdate + = ProjectContributionUpdate + { title : String + , description : Maybe String + , status : ContributionStatus + , sourceBranchRef : BranchRef + , targetBranchRef : BranchRef + } + | ProjectContributionTitleUpdate String + | ProjectContributionDescriptionUpdate String + | ProjectContributionStatusUpdate ContributionStatus + + +updateProjectContribution : ProjectRef -> ContributionRef -> ProjectContributionUpdate -> Endpoint +updateProjectContribution projectRef contribRef update = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + case update of + ProjectContributionUpdate update_ -> + Encode.object + [ ( "title", Encode.string update_.title ) + , ( "description", MaybeE.unwrap Encode.null Encode.string update_.description ) + , ( "status", Encode.string (ContributionStatus.toApiString update_.status) ) + , ( "sourceBranchRef", Encode.string (BranchRef.toString update_.sourceBranchRef) ) + , ( "targetBranchRef", Encode.string (BranchRef.toString update_.targetBranchRef) ) + ] + + ProjectContributionTitleUpdate title -> + Encode.object + [ ( "title", Encode.string title ) + ] + + ProjectContributionDescriptionUpdate description -> + Encode.object + [ ( "description", Encode.string description ) + ] + + ProjectContributionStatusUpdate status -> + Encode.object + [ ( "status", Encode.string (ContributionStatus.toApiString status) ) + ] + in + PATCH + { path = + [ "users" + , handle + , "projects" + , slug + , "contributions" + , ContributionRef.toApiString contribRef + ] + , queryParams = [] + , body = Http.jsonBody body + } + + +projectContributionTimeline : ProjectRef -> ContributionRef -> Endpoint +projectContributionTimeline projectRef contribRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "contributions", ContributionRef.toApiString contribRef, "timeline" ] + , queryParams = [] + } + + +projectContributions : ProjectRef -> Endpoint +projectContributions projectRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "contributions" ] + , queryParams = [] + } + + +projectContributionDiff : ProjectRef -> ContributionRef -> Endpoint +projectContributionDiff projectRef contribRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "contributions", ContributionRef.toApiString contribRef, "diff" ] + , queryParams = [] + } + + +projectBranchDiff : ProjectRef -> BranchRef -> BranchRef -> Endpoint +projectBranchDiff projectRef branchA branchB = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + queryParams = + [ string "from" (BranchRef.toApiUrlString branchA) + , string "to" (BranchRef.toApiUrlString branchB) + ] + in + GET + { path = [ "users", handle, "projects", slug, "diff", "branches" ] + , queryParams = queryParams + } + + + +-- PROJECT TICKETS + + +projectTicket : ProjectRef -> TicketRef -> Endpoint +projectTicket projectRef ticketRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "tickets", TicketRef.toApiString ticketRef ] + , queryParams = [] + } + + +type alias NewProjectTicket = + { title : String + , description : String + , status : TicketStatus + } + + +createProjectTicket : ProjectRef -> NewProjectTicket -> Endpoint +createProjectTicket projectRef data = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object + [ ( "title", Encode.string data.title ) + , ( "description", Encode.string data.description ) + , ( "status", Encode.string (TicketStatus.toApiString data.status) ) + ] + in + POST + { path = [ "users", handle, "projects", slug, "tickets" ] + , queryParams = [] + , body = Http.jsonBody body + } + + +createProjectTicketComment : ProjectRef -> TicketRef -> String -> Endpoint +createProjectTicketComment projectRef ticketRef comment = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object + [ ( "content", Encode.string comment ) + ] + in + POST + { path = + [ "users" + , handle + , "projects" + , slug + , "tickets" + , TicketRef.toApiString ticketRef + , "timeline" + , "comments" + ] + , queryParams = [] + , body = Http.jsonBody body + } + + +updateProjectTicketComment : ProjectRef -> TicketRef -> CommentId -> Int -> String -> Endpoint +updateProjectTicketComment projectRef ticketRef commentId originalRevision comment = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object + [ ( "content", Encode.string comment ) + , ( "expectedRevision", Encode.int originalRevision ) + ] + in + PATCH + { path = + [ "users" + , handle + , "projects" + , slug + , "tickets" + , TicketRef.toApiString ticketRef + , "timeline" + , "comments" + , CommentId.toString commentId + ] + , queryParams = [] + , body = Http.jsonBody body + } + + +deleteProjectTicketComment : ProjectRef -> TicketRef -> CommentId -> Endpoint +deleteProjectTicketComment projectRef ticketRef commentId = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + DELETE + { path = + [ "users" + , handle + , "projects" + , slug + , "tickets" + , TicketRef.toApiString ticketRef + , "timeline" + , "comments" + , CommentId.toString commentId + ] + , queryParams = [] + } + + +type ProjectTicketUpdate + = ProjectTicketUpdate + { title : String + , description : String + , status : TicketStatus + } + | ProjectTicketTitleUpdate String + | ProjectTicketDescriptionUpdate String + | ProjectTicketStatusUpdate TicketStatus + + +updateProjectTicket : ProjectRef -> TicketRef -> ProjectTicketUpdate -> Endpoint +updateProjectTicket projectRef ticketRef update = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + case update of + ProjectTicketUpdate update_ -> + Encode.object + [ ( "title", Encode.string update_.title ) + , ( "description", Encode.string update_.description ) + , ( "status", Encode.string (TicketStatus.toApiString update_.status) ) + ] + + ProjectTicketTitleUpdate title -> + Encode.object + [ ( "title", Encode.string title ) + ] + + ProjectTicketDescriptionUpdate description -> + Encode.object + [ ( "description", Encode.string description ) + ] + + ProjectTicketStatusUpdate status -> + Encode.object + [ ( "status", Encode.string (TicketStatus.toApiString status) ) + ] + in + PATCH + { path = + [ "users" + , handle + , "projects" + , slug + , "tickets" + , TicketRef.toApiString ticketRef + ] + , queryParams = [] + , body = Http.jsonBody body + } + + +projectTicketTimeline : ProjectRef -> TicketRef -> Endpoint +projectTicketTimeline projectRef ticketRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = + [ "users" + , handle + , "projects" + , slug + , "tickets" + , TicketRef.toApiString ticketRef + , "timeline" + ] + , queryParams = [] + } + + +projectTickets : ProjectRef -> Endpoint +projectTickets projectRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "tickets" ] + , queryParams = [] + } + + + +-- PROJECT RELEASES + + +projectRelease : ProjectRef -> Version -> Endpoint +projectRelease projectRef version = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "releases", Version.toString version ] + , queryParams = [] + } + + +projectReleases : ProjectRef -> Endpoint +projectReleases projectRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "releases" ] + , queryParams = [] + } + + +projectReleaseNotes : ProjectRef -> Version -> Endpoint +projectReleaseNotes projectRef version = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = + [ "users" + , handle + , "projects" + , slug + , "releases" + , Version.toString version + , "releaseNotes" + ] + , queryParams = [] + } + + +createProjectRelease : + ProjectRef + -> { branchRef : BranchRef, causalHash : Hash, version : Version } + -> Endpoint +createProjectRelease projectRef data = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object + [ ( "branchRef", Encode.string (BranchRef.toString data.branchRef) ) + , ( "causalHash", Encode.string (Hash.toString data.causalHash) ) + , ( "major", Encode.int (Version.major data.version) ) + , ( "minor", Encode.int (Version.minor data.version) ) + , ( "patch", Encode.int (Version.patch data.version) ) + ] + in + POST + { path = [ "users", handle, "projects", slug, "releases" ] + , queryParams = [] + , body = Http.jsonBody body + } + + +updateProjectFav : ProjectRef -> Bool -> Endpoint +updateProjectFav projectRef isFaved = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + Encode.object [ ( "isFaved", Encode.bool isFaved ) ] + |> Http.jsonBody + in + PUT + { path = [ "users", handle, "projects", slug, "fav" ] + , queryParams = [] + , body = body + } + + +type ProjectUpdate + = ProjectDescriptionUpdate { summary : Maybe String, tags : Set String } + | ProjectSettingsUpdate { visibility : ProjectVisibility } + + +updateProject : ProjectRef -> ProjectUpdate -> Endpoint +updateProject projectRef update = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + + body = + case update of + ProjectDescriptionUpdate changes -> + Encode.object + [ ( "summary", EncodeE.maybe Encode.string changes.summary ) + , ( "tags" + , Encode.object + [ ( "replaceWith" + , Encode.list Encode.string (Set.toList changes.tags) + ) + ] + ) + ] + |> Http.jsonBody + + ProjectSettingsUpdate changes -> + Encode.object + [ ( "visibility" + , Encode.string (Project.visibilityToString changes.visibility) + ) + ] + |> Http.jsonBody + in + PATCH + { path = [ "users", handle, "projects", slug ] + , queryParams = [] + , body = body + } + + +deleteProject : ProjectRef -> Endpoint +deleteProject projectRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + DELETE + { path = [ "users", handle, "projects", slug ] + , queryParams = [] + } + + +deleteProjectBranch : ProjectRef -> BranchRef -> Endpoint +deleteProjectBranch projectRef branchRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + DELETE + { path = [ "users", handle, "projects", slug, "branches", BranchRef.toApiUrlString branchRef ] + , queryParams = [] + } + + +projectReadme : ProjectRef -> Endpoint +projectReadme projectRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET { path = [ "users", handle, "projects", slug, "readme" ], queryParams = [] } + + +projects : Maybe String -> Endpoint +projects owner = + let + queryParams = + case owner of + Just owner_ -> + [ string "owner" owner_ ] + + Nothing -> + [] + in + GET { path = [ "projects" ], queryParams = queryParams } + + +catalog : Endpoint +catalog = + GET { path = [ "catalog" ], queryParams = [] } + + + +-- CODE BROWSING + + +baseCodePathFromContext : CodeBrowsingContext -> List String +baseCodePathFromContext context = + case context of + UserCode h -> + [ "codebases", UserHandle.toUnprefixedString h ] + + ProjectBranch pr br -> + let + ( handle, slug ) = + ProjectRef.toApiStringParts pr + in + case br of + BranchRef.ReleaseBranchRef v -> + [ "users", handle, "projects", slug, "releases", Version.toString v ] + + _ -> + [ "users", handle, "projects", slug, "branches", BranchRef.toApiUrlString br ] + + +namespace : CodeBrowsingContext -> Perspective -> FQN -> Endpoint +namespace context perspective fqn = + let + queryParams = + [ toRootHash (Perspective.rootPerspective perspective) ] + + base = + baseCodePathFromContext context + in + GET + { path = base ++ [ "namespaces", "by-name", FQN.toApiUrlString fqn ] + , queryParams = MaybeE.values queryParams + } + + +browseCodebase : CodeBrowsingContext -> Perspective -> Maybe NamespaceRef -> Endpoint +browseCodebase context perspective ref = + let + base = + baseCodePathFromContext context + + namespace_ = + ref + |> Maybe.map NamespaceRef.toString + |> Maybe.map (string "namespace") + |> Maybe.map (\qp -> [ qp ]) + in + GET + { path = base ++ [ "browse" ] + , queryParams = Maybe.withDefault [] namespace_ ++ perspectiveToQueryParams perspective + } + + +codebaseApiEndpointToEndpoint : CodeBrowsingContext -> CodebaseApi.CodebaseEndpoint -> Endpoint +codebaseApiEndpointToEndpoint context cbEndpoint = + let + base = + baseCodePathFromContext context + in + case cbEndpoint of + CodebaseApi.Find { perspective, withinFqn, limit, sourceWidth, query } -> + let + params = + case withinFqn of + Just fqn -> + [ toRootHash (Perspective.rootPerspective perspective) + , Just (relativeTo fqn) + ] + |> MaybeE.values + + Nothing -> + perspectiveToQueryParams perspective + + width = + case sourceWidth of + Syntax.Width w -> + w + in + GET + { path = base ++ [ "find" ] + , queryParams = + [ int "limit" limit + , int "renderWidth" width + , string "query" query + ] + ++ params + } + + CodebaseApi.Browse { perspective, ref } -> + browseCodebase context perspective ref + + CodebaseApi.Definition { perspective, ref } -> + let + -- TODO: Temporarily disable constructor suffixes in hashes + constructorSuffixRegex = + Maybe.withDefault Regex.never (Regex.fromString "@[ad]\\d$") + + withoutConstructorSuffix h = + h + |> Hash.toApiUrlString + |> Regex.replace constructorSuffixRegex (always "") + + path = + case Reference.hashQualified ref of + HQ.NameOnly fqn -> + [ "definitions", "by-name", FQN.toApiUrlString fqn ] + + HQ.HashOnly h -> + [ "definitions", "by-hash", withoutConstructorSuffix h ] + + HQ.HashQualified _ h -> + [ "definitions", "by-hash", withoutConstructorSuffix h ] + in + GET + { path = base ++ path + , queryParams = perspectiveToQueryParams perspective + } + + CodebaseApi.Summary { perspective, ref } -> + let + hqUrl hq = + case hq of + HQ.NameOnly fqn -> + -- TODO: Not really valid... + { path = [ "by-name", FQN.toApiUrlString fqn ] + , queryParams = [] + } + + HQ.HashOnly h -> + { path = [ "by-hash", Hash.toApiUrlString h ] + , queryParams = [] + } + + HQ.HashQualified fqn h -> + { path = [ "by-hash", Hash.toApiUrlString h ] + , queryParams = [ FQN.toQueryString fqn ] + } + + { path, queryParams } = + case ref of + Reference.TermReference hq -> + let + hqUrl_ = + hqUrl hq + in + { hqUrl_ | path = [ "definitions", "terms" ] ++ hqUrl_.path ++ [ "summary" ] } + + Reference.TypeReference hq -> + let + hqUrl_ = + hqUrl hq + in + { hqUrl_ | path = [ "definitions", "types" ] ++ hqUrl_.path ++ [ "summary" ] } + + Reference.AbilityConstructorReference hq -> + let + hqUrl_ = + hqUrl hq + in + { hqUrl_ | path = [ "definitions", "terms" ] ++ hqUrl_.path ++ [ "summary" ] } + + Reference.DataConstructorReference hq -> + let + hqUrl_ = + hqUrl hq + in + { hqUrl_ | path = [ "definitions", "terms" ] ++ hqUrl_.path ++ [ "summary" ] } + in + GET + { path = base ++ path + , queryParams = queryParams ++ perspectiveToQueryParams perspective + } + + + +-- QUERY PARAMS --------------------------------------------------------------- + + +perspectiveToQueryParams : Perspective -> List QueryParameter +perspectiveToQueryParams perspective = + case perspective of + Root p -> + MaybeE.values [ toRootHash p.root ] + + Namespace d -> + [ toRootHash d.root, Just (relativeTo d.fqn) ] |> MaybeE.values + + +toRootHash : Perspective.RootPerspective -> Maybe QueryParameter +toRootHash rootPerspective = + case rootPerspective of + Perspective.Relative -> + Nothing + + Perspective.Absolute h -> + Just (rootHash h) + + +rootHash : Hash -> QueryParameter +rootHash hash = + string "rootHash" (hash |> Hash.toUnprefixedString) + + +relativeTo : FQN -> QueryParameter +relativeTo fqn = + string "relativeTo" (fqn |> FQN.toString) diff --git a/src/UnisonShare/App.elm b/src/UnisonShare/App.elm new file mode 100644 index 00000000..fbfd2e2f --- /dev/null +++ b/src/UnisonShare/App.elm @@ -0,0 +1,715 @@ +module UnisonShare.App exposing (..) + +import Browser +import Browser.Navigation as Nav +import Html + exposing + ( Html + , aside + , br + , div + , footer + , h2 + , h3 + , header + , li + , p + , section + , span + , text + , ul + ) +import Html.Attributes exposing (class) +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.OperatingSystem exposing (OperatingSystem(..)) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Lib.Util as Util +import Time +import UI +import UI.Avatar as Avatar +import UI.Button as Button +import UI.DateTime as DateTime +import UI.Icon as Icon +import UI.KeyboardShortcut as KeyboardShortcut +import UI.KeyboardShortcut.Key as Key exposing (Key(..)) +import UI.Modal as Modal +import UnisonShare.Account as Account exposing (Account) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.AppDocument as AppDocument +import UnisonShare.AppError as AppError exposing (AppError) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.Link as Link +import UnisonShare.Page.AcceptTermsPage as AcceptTermsPage +import UnisonShare.Page.AccountPage as AccountPage +import UnisonShare.Page.AppErrorPage as AppErrorPage +import UnisonShare.Page.CatalogPage as CatalogPage +import UnisonShare.Page.CloudPage as CloudPage +import UnisonShare.Page.NotFoundPage as NotFoundPage +import UnisonShare.Page.PrivacyPolicyPage as PrivacyPolicyPage +import UnisonShare.Page.ProjectPage as ProjectPage +import UnisonShare.Page.TermsOfServicePage as TermsOfServicePage +import UnisonShare.Page.UcmConnectedPage as UcmConnectedPage +import UnisonShare.Page.UserPage as UserPage +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Route as Route exposing (Route) +import UnisonShare.Session as Session +import UnisonShare.Tour as Tour +import Url exposing (Url) +import WhatsNew exposing (WhatsNew) + + + +-- MODEL + + +type Page + = Catalog CatalogPage.Model + | Account AccountPage.Model + | User UserHandle Route.UserRoute UserPage.Model + | Project ProjectRef Route.ProjectRoute ProjectPage.Model + | TermsOfService + | AcceptTerms (Maybe Url) AcceptTermsPage.Model + | PrivacyPolicy + | UcmConnected + | Cloud + | Error AppError + | NotFound + + +type AppModal + = NoModal + | KeyboardShortcuts + + +type alias Model = + { page : Page + , appContext : AppContext + , openedAppHeaderMenu : AppHeader.OpenedAppHeaderMenu + , appModal : AppModal + , whatsNew : WhatsNew + } + + +init : AppContext -> Route -> ( Model, Cmd Msg ) +init appContext route = + let + ( page, cmd ) = + case route of + Route.Catalog -> + let + ( catalog, catalogCmd ) = + CatalogPage.init appContext + in + ( Catalog catalog, Cmd.map CatalogPageMsg catalogCmd ) + + Route.Account -> + let + account = + AccountPage.init + in + ( Account account, Cmd.none ) + + Route.User handle userRoute -> + let + ( user, userCmd ) = + UserPage.init appContext handle userRoute + in + ( User handle userRoute user, Cmd.map UserPageMsg userCmd ) + + Route.Project projectRef projectRoute -> + let + ( project, userCmd ) = + ProjectPage.init appContext projectRef projectRoute + in + ( Project projectRef projectRoute project, Cmd.map ProjectPageMsg userCmd ) + + Route.TermsOfService -> + ( TermsOfService, Cmd.none ) + + Route.AcceptTerms continueUrl -> + ( AcceptTerms continueUrl AcceptTermsPage.init, Cmd.none ) + + Route.PrivacyPolicy -> + ( PrivacyPolicy, Cmd.none ) + + Route.UcmConnected -> + ( UcmConnected, Cmd.none ) + + Route.Cloud -> + ( Cloud, Cmd.none ) + + Route.Error e -> + ( Error e, Cmd.none ) + + Route.NotFound _ -> + ( NotFound, Cmd.none ) + + model = + { page = page + , appContext = appContext + , openedAppHeaderMenu = AppHeader.NoneOpened + , appModal = NoModal + , whatsNew = WhatsNew.Loading (List.map WhatsNew.PostId appContext.whatsNewReadPostIds) + } + in + ( model + , Cmd.batch + [ cmd + , WhatsNew.fetchFeed appContext model.whatsNew WhatsNewFetchFinished + ] + ) + + + +-- UPDATE + + +type Msg + = NoOp + | Tick Time.Posix + | LinkClicked Browser.UrlRequest + | UrlChanged Url + | AcceptWelcomeTerms + | ToggleHelpAndResourcesMenu + | ToggleAccountMenu + | ToggleCreateAccountMenu + | ShowKeyboardShortcuts + | CloseModal + | WhatsNewFetchFinished (HttpResult WhatsNew.LoadedWhatsNew) + | WhatsNewMarkAllAsRead + -- Sub msgs + | CatalogPageMsg CatalogPage.Msg + | UserPageMsg UserPage.Msg + | ProjectPageMsg ProjectPage.Msg + | AccountPageMsg AccountPage.Msg + | AcceptTermsPageMsg AcceptTermsPage.Msg + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg ({ appContext } as model) = + case ( model.page, msg ) of + ( _, Tick t ) -> + let + appContext_ = + { appContext | now = DateTime.fromPosix t } + in + ( { model | appContext = appContext_ }, Cmd.none ) + + ( _, LinkClicked urlRequest ) -> + case urlRequest of + Browser.Internal url -> + ( model, Nav.pushUrl appContext.navKey (Url.toString url) ) + + -- External links are handled via target blank and never end up + -- here except for login and logout + Browser.External url -> + if String.contains "logout" url || String.contains "login" url then + ( model, Nav.load url ) + + else + ( model, Cmd.none ) + + ( _, UrlChanged url ) -> + let + route = + Route.fromUrl appContext.basePath url + + appContext_ = + { appContext | currentUrl = url } + + model_ = + { model | appContext = appContext_ } + + ( m, c ) = + case route of + Route.Catalog -> + let + ( catalog, cmd ) = + CatalogPage.init appContext_ + in + ( { model_ | page = Catalog catalog }, Cmd.map CatalogPageMsg cmd ) + + Route.Account -> + ( { model_ | page = Account AccountPage.init }, Cmd.none ) + + Route.User handle userRoute -> + case model_.page of + User currentHandle _ userModel -> + if UserHandle.equals currentHandle handle then + let + ( user, cmd ) = + UserPage.updateSubPage appContext_ handle userModel userRoute + in + ( { model_ | page = User handle userRoute user }, Cmd.map UserPageMsg cmd ) + + else + let + ( user, cmd ) = + UserPage.init appContext_ handle userRoute + in + ( { model_ | page = User handle userRoute user }, Cmd.map UserPageMsg cmd ) + + _ -> + let + ( user, cmd ) = + UserPage.init appContext_ handle userRoute + in + ( { model_ | page = User handle userRoute user }, Cmd.map UserPageMsg cmd ) + + Route.Project projectRef projectRoute -> + case model_.page of + Project currentProjectRef _ projectModel -> + if ProjectRef.equals currentProjectRef projectRef then + let + ( project, cmd ) = + ProjectPage.updateSubPage appContext_ projectRef projectModel projectRoute + in + ( { model_ | page = Project projectRef projectRoute project }, Cmd.map ProjectPageMsg cmd ) + + else + let + ( project, cmd ) = + ProjectPage.init appContext_ projectRef projectRoute + in + ( { model_ | page = Project projectRef projectRoute project }, Cmd.map ProjectPageMsg cmd ) + + _ -> + let + ( project, cmd ) = + ProjectPage.init appContext_ projectRef projectRoute + in + ( { model_ | page = Project projectRef projectRoute project }, Cmd.map ProjectPageMsg cmd ) + + Route.TermsOfService -> + ( { model_ | page = TermsOfService }, Cmd.none ) + + Route.AcceptTerms continueUrl -> + ( { model_ | page = AcceptTerms continueUrl AcceptTermsPage.init }, Cmd.none ) + + Route.PrivacyPolicy -> + ( { model_ | page = PrivacyPolicy }, Cmd.none ) + + Route.UcmConnected -> + ( { model_ | page = UcmConnected }, Cmd.none ) + + Route.Cloud -> + ( { model_ | page = Cloud }, Cmd.none ) + + Route.Error e -> + ( { model_ | page = Error e }, Cmd.none ) + + Route.NotFound _ -> + ( { model_ | page = NotFound }, Cmd.none ) + in + ( m, c ) + + ( _, WhatsNewFetchFinished r ) -> + let + whatsNew = + case r of + Ok wn -> + WhatsNew.Success wn + + Err _ -> + let + readPostIds = + case model.whatsNew of + WhatsNew.Loading ids -> + ids + + WhatsNew.Success d -> + d.readPostIds + + WhatsNew.Failure ids -> + ids + in + WhatsNew.Failure readPostIds + in + ( { model | whatsNew = whatsNew }, Cmd.none ) + + ( _, AcceptWelcomeTerms ) -> + case appContext.session of + Session.SignedIn a -> + ( { model + | appContext = + { appContext + | session = Session.SignedIn (Account.markTourAsCompleted Tour.WelcomeTerms a) + } + } + , completeWelcomeTour appContext + ) + + Session.Anonymous -> + ( model, Cmd.none ) + + ( _, WhatsNewMarkAllAsRead ) -> + let + ( wn, cmd ) = + WhatsNew.markAllAsRead model.whatsNew + in + ( { model | whatsNew = wn }, cmd ) + + ( _, ToggleHelpAndResourcesMenu ) -> + let + ( openedAppHeaderMenu, cmd ) = + if model.openedAppHeaderMenu == AppHeader.HelpAndResourcesMenu then + ( AppHeader.NoneOpened, Cmd.none ) + + else + ( AppHeader.HelpAndResourcesMenu, Util.delayMsg 2500 WhatsNewMarkAllAsRead ) + in + ( { model | openedAppHeaderMenu = openedAppHeaderMenu }, cmd ) + + ( _, ToggleAccountMenu ) -> + let + openedAppHeaderMenu = + if model.openedAppHeaderMenu == AppHeader.AccountMenu then + AppHeader.NoneOpened + + else + AppHeader.AccountMenu + in + ( { model | openedAppHeaderMenu = openedAppHeaderMenu }, Cmd.none ) + + ( _, ToggleCreateAccountMenu ) -> + let + openedAppHeaderMenu = + if model.openedAppHeaderMenu == AppHeader.CreateAccountMenu then + AppHeader.NoneOpened + + else + AppHeader.CreateAccountMenu + in + ( { model | openedAppHeaderMenu = openedAppHeaderMenu }, Cmd.none ) + + ( _, ShowKeyboardShortcuts ) -> + ( { model | openedAppHeaderMenu = AppHeader.NoneOpened, appModal = KeyboardShortcuts }, Cmd.none ) + + ( _, CloseModal ) -> + ( { model | appModal = NoModal }, Cmd.none ) + + -- Sub msgs + ( Catalog m, CatalogPageMsg cMsg ) -> + let + ( catalog, cmd ) = + CatalogPage.update appContext cMsg m + in + ( { model | page = Catalog catalog }, Cmd.map CatalogPageMsg cmd ) + + ( User handle route m, UserPageMsg uMsg ) -> + let + ( user, cmd ) = + UserPage.update appContext handle route uMsg m + in + ( { model | page = User handle route user }, Cmd.map UserPageMsg cmd ) + + ( Project projectRef route m, ProjectPageMsg pMsg ) -> + let + ( project, cmd ) = + ProjectPage.update appContext projectRef route pMsg m + in + ( { model | page = Project projectRef route project }, Cmd.map ProjectPageMsg cmd ) + + ( Account m, AccountPageMsg aMsg ) -> + case appContext.session of + Session.SignedIn a -> + let + ( accountM, cmd ) = + AccountPage.update appContext a aMsg m + in + ( { model | page = Account accountM }, Cmd.map AccountPageMsg cmd ) + + _ -> + ( model, Cmd.none ) + + ( AcceptTerms continueUrl acceptTerms, AcceptTermsPageMsg atMsg ) -> + let + ( acceptTerms_, acceptTermsCmd ) = + AcceptTermsPage.update appContext atMsg continueUrl acceptTerms + in + ( { model | page = AcceptTerms continueUrl acceptTerms_ }, Cmd.map AcceptTermsPageMsg acceptTermsCmd ) + + _ -> + ( model, Cmd.none ) + + + +-- EFFECTS + + +completeWelcomeTour : AppContext -> Cmd Msg +completeWelcomeTour appContext = + ShareApi.completeTours [ Tour.WelcomeTerms ] + |> HttpApi.toRequestWithEmptyResponse (\_ -> NoOp) + |> HttpApi.perform appContext.api + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions model = + let + sub = + case model.page of + User _ _ up -> + Sub.map UserPageMsg (UserPage.subscriptions up) + + Project _ _ pp -> + Sub.map ProjectPageMsg (ProjectPage.subscriptions pp) + + _ -> + Sub.none + in + Sub.batch [ sub, Time.every 1000 Tick ] + + + +-- VIEW + + +viewKeyboardShortcutsModal : OperatingSystem -> Html Msg +viewKeyboardShortcutsModal os = + let + -- The shortcut views requires a model, but we don't really need it for this kind of overview + keyboardShortcut = + KeyboardShortcut.init os + + viewRow label instructions = + div + [ class "row" ] + [ label + , div [ class "instructions" ] instructions + ] + + viewInstructions label shortcuts = + viewRow label [ KeyboardShortcut.viewShortcuts keyboardShortcut shortcuts ] + + openFinderInstructions = + case os of + MacOS -> + [ KeyboardShortcut.Chord Meta (K Key.Lower), KeyboardShortcut.Chord Ctrl (K Key.Lower), KeyboardShortcut.single ForwardSlash ] + + _ -> + [ KeyboardShortcut.Chord Ctrl (K Key.Lower), KeyboardShortcut.single ForwardSlash ] + + toggleSidebarInstructions = + case os of + MacOS -> + [ KeyboardShortcut.Chord Meta (B Key.Lower), KeyboardShortcut.Chord Ctrl (B Key.Lower) ] + + _ -> + [ KeyboardShortcut.Chord Ctrl (B Key.Lower) ] + + content = + Modal.Content + (section + [ class "shortcuts" ] + [ div [ class "shortcut-group" ] + [ h3 [] [ text "Browsing Code" ] + , viewInstructions (text "Open Finder") openFinderInstructions + , viewInstructions (text "Toggle sidebar") toggleSidebarInstructions + , viewInstructions (text "Move focus up") [ KeyboardShortcut.single ArrowUp, KeyboardShortcut.single (K Key.Lower) ] + , viewInstructions (text "Move focus down") [ KeyboardShortcut.single ArrowDown, KeyboardShortcut.single (J Key.Lower) ] + , viewInstructions (text "Close focused definition") [ KeyboardShortcut.single (X Key.Lower) ] + , viewInstructions (text "Expand/Collapse focused definition") [ KeyboardShortcut.single Space ] + ] + , div [ class "shortcut-group" ] + [ h3 [] [ text "Finder" ] + , viewInstructions (text "Clear search query") [ KeyboardShortcut.single Escape ] + , viewInstructions (span [] [ text "Close", UI.subtle " (when search query is empty)" ]) [ KeyboardShortcut.single Escape ] + , viewInstructions (text "Move focus up") [ KeyboardShortcut.single ArrowUp ] + , viewInstructions (text "Move focus down") [ KeyboardShortcut.single ArrowDown ] + , viewInstructions (text "Open focused definition") [ KeyboardShortcut.single Enter ] + , viewRow (text "Open definition") + [ KeyboardShortcut.viewBase + [ KeyboardShortcut.viewKey os Semicolon False + , KeyboardShortcut.viewThen + , KeyboardShortcut.viewKeyBase "1-9" False + ] + ] + ] + ] + ) + in + Modal.modal "help-modal" CloseModal content + |> Modal.withHeader "Keyboard shortcuts" + |> Modal.view + + +viewWelcomeTermsModal : Account a -> Html Msg +viewWelcomeTermsModal account = + let + avatar = + Account.toAvatar account |> Avatar.view + + content = + div [] + [ header [ class "welcome" ] + [ div [ class "avatar-container" ] [ avatar ] + , div [] + [ h2 [] [ Icon.view Icon.tada, text "Welcome" ] + , p [] + [ text + ("Hello " ++ Account.name account ++ ", and welcome to Unison Share!") + , br [] [] + , text "We're really excited that you are here and can't wait to see what you do with Unison." + ] + , p [] [ text "If you haven’t already, consider joining our", Button.iconThenLabel_ Link.discord Icon.discord "Discord Community" |> Button.small |> Button.view ] + ] + ] + , UI.divider + , div [] + [ h2 [] [ Icon.view Icon.star, text "Membership Tenets" ] + , div [ class "membership-tenets" ] + [ div [ class "tenets" ] + [ p [] + [ text "Share is not just a code hosting service, it’s also a place for the Unison community to learn from one another, collaborate, and connect." + ] + , p [] + [ text "Membership here means you are an important steward of our community. In addition to the " + , Link.view "Code of Conduct" Link.codeOfConduct + , text " and " + , Link.view "Terms of Service" Link.termsOfService + , text ", you can help us create an inclusive, welcoming space by:" + ] + , ul [] + [ li [] [ Icon.view Icon.dot, text "Sharing knowledge and encouragement generously 🎁" ] + , li [] [ Icon.view Icon.dot, text "Being kind to each other 💖" ] + , li [] [ Icon.view Icon.dot, text "Being respectful and empathetic when disagreeing 🤝" ] + , li [] [ Icon.view Icon.dot, text "Supporting people of all skill levels 🐣" ] + , li [] [ Icon.view Icon.dot, text "Keeping our community free of discrimination, contempt, or harassment ☮️" ] + ] + ] + , aside [ class "documents" ] + [ p [] [ text "By using Unison Share you agree to the following" ] + , ul [] + [ li [] [ Icon.view Icon.heart, Link.view "Code of Conduct" Link.codeOfConduct ] + , li [] [ Icon.view Icon.certificate, Link.view "Terms of Service" Link.termsOfService ] + , li [] [ Icon.view Icon.eyeSlash, Link.view "Privacy Policy" Link.privacyPolicy ] + ] + ] + ] + ] + , footer [] + [ Button.iconThenLabel AcceptWelcomeTerms Icon.checkmark "Accept and Continue to Unison Share" + |> Button.positive + |> Button.medium + |> Button.view + ] + ] + in + Modal.modal_ + "welcome-tour-modal" + (Modal.Content content) + |> Modal.view + + +view : Model -> Browser.Document Msg +view model = + let + appContext = + model.appContext + + session = + appContext.session + + appHeaderContext = + { session = session + , timeZone = appContext.timeZone + , currentUrl = appContext.currentUrl + , whatsNew = model.whatsNew + , api = appContext.api + , openedAppHeaderMenu = model.openedAppHeaderMenu + , toggleHelpAndResourcesMenuMsg = ToggleHelpAndResourcesMenu + , toggleAccountMenuMsg = ToggleAccountMenu + , toggleCreateAccountMenuMsg = ToggleCreateAccountMenu + , showKeyboardShortcutsModalMsg = ShowKeyboardShortcuts + } + + appDocument = + case model.page of + Catalog catalog -> + AppDocument.map CatalogPageMsg (CatalogPage.view catalog) + + Account accountModel -> + case session of + Session.SignedIn a -> + AppDocument.map AccountPageMsg (AccountPage.view a accountModel) + + Session.Anonymous -> + NotFoundPage.view + + User handle _ userModel -> + AppDocument.map UserPageMsg (UserPage.view appContext handle userModel) + + Project projectRef _ projectModel -> + AppDocument.map ProjectPageMsg (ProjectPage.view appContext projectRef projectModel) + + TermsOfService -> + TermsOfServicePage.view + + AcceptTerms _ acceptTerms -> + acceptTerms + |> AcceptTermsPage.view + |> AppDocument.map AcceptTermsPageMsg + + PrivacyPolicy -> + PrivacyPolicyPage.view + + UcmConnected -> + case appContext.session of + Session.Anonymous -> + AppErrorPage.view appContext AppError.UnspecifiedError + + Session.SignedIn a -> + UcmConnectedPage.view a + + Cloud -> + CloudPage.view session + + Error error -> + AppErrorPage.view appContext error + + NotFound -> + NotFoundPage.view + + -- Overwrite Modal with any app level, user initiated modal + appDocumentWithModal = + case model.appModal of + NoModal -> + appDocument + + KeyboardShortcuts -> + { appDocument | modal = Just (viewKeyboardShortcutsModal appContext.operatingSystem) } + + appDocumentWithWelcomeTermsModal = + -- We link to TermsOfService and PrivacyPolicy from the welcome + -- terms modal and the AcceptTerms page is used during UCM signup, + -- so we don't want to block those pages by the welcome modal. + case ( model.page, model.appContext.session ) of + ( TermsOfService, _ ) -> + appDocumentWithModal + + ( AcceptTerms _ _, _ ) -> + appDocumentWithModal + + ( PrivacyPolicy, _ ) -> + appDocumentWithModal + + ( _, Session.SignedIn a ) -> + if not (Account.hasCompletedTour Tour.WelcomeTerms a) then + { appDocumentWithModal + | modal = + Just (viewWelcomeTermsModal a) + } + + else + appDocumentWithModal + + _ -> + appDocumentWithModal + in + AppDocument.view appHeaderContext appDocumentWithWelcomeTermsModal diff --git a/src/UnisonShare/AppContext.elm b/src/UnisonShare/AppContext.elm new file mode 100644 index 00000000..bcb0b803 --- /dev/null +++ b/src/UnisonShare/AppContext.elm @@ -0,0 +1,66 @@ +module UnisonShare.AppContext exposing (..) + +import Browser.Navigation as Nav +import Code.Config +import Code.Perspective exposing (Perspective) +import Lib.HttpApi as HttpApi exposing (HttpApi) +import Lib.OperatingSystem as OS exposing (OperatingSystem) +import Time +import UI.DateTime exposing (DateTime) +import UnisonShare.Api as Api +import UnisonShare.CodeBrowsingContext exposing (CodeBrowsingContext) +import UnisonShare.Session exposing (Session) +import Url exposing (Url) + + +type alias AppContext = + { operatingSystem : OperatingSystem + , basePath : String + , currentUrl : Url + , api : HttpApi + , websiteApi : HttpApi + , navKey : Nav.Key + , session : Session + , whatsNewReadPostIds : List String + , now : DateTime + , timeZone : Time.Zone + } + + +type alias Flags = + { operatingSystem : String + , basePath : String + , apiUrl : String + , websiteUrl : String + , xsrfToken : Maybe String + , appEnv : String + , whatsNewReadPostIds : List String + } + + +init : Flags -> Nav.Key -> Url -> DateTime -> Time.Zone -> Session -> AppContext +init flags navKey currentUrl now timeZone session = + let + api = + HttpApi.httpApi True flags.apiUrl flags.xsrfToken + in + { operatingSystem = OS.fromString flags.operatingSystem + , basePath = flags.basePath + , currentUrl = currentUrl + , api = api + , websiteApi = HttpApi.httpApi False flags.websiteUrl Nothing + , navKey = navKey + , session = session + , whatsNewReadPostIds = flags.whatsNewReadPostIds + , now = now + , timeZone = timeZone + } + + +toCodeConfig : AppContext -> CodeBrowsingContext -> Perspective -> Code.Config.Config +toCodeConfig appContex codeBrowsingContext perspective = + { operatingSystem = appContex.operatingSystem + , perspective = perspective + , toApiEndpoint = Api.codebaseApiEndpointToEndpoint codeBrowsingContext + , api = appContex.api + } diff --git a/src/UnisonShare/AppDocument.elm b/src/UnisonShare/AppDocument.elm new file mode 100644 index 00000000..76f28e77 --- /dev/null +++ b/src/UnisonShare/AppDocument.elm @@ -0,0 +1,108 @@ +module UnisonShare.AppDocument exposing (AppDocument, appDocument, map, view, withModal) + +import Browser exposing (Document) +import Html exposing (Html, div) +import Html.Attributes exposing (class, id) +import Maybe.Extra as MaybeE +import UI +import UI.PageHeader as PageHeader exposing (PageHeader) +import UnisonShare.AppHeader as AppHeader exposing (AppHeader, AppHeaderContext) + + + +{- + + AppDocument + =========== + + Very similar to Browser.Document, but includes a common app title and app + frame, as well as slots for header, page, and modals. +-} + + +type alias AppDocument msg = + { pageId : String + , title : String + , appHeader : AppHeader + , pageHeader : Maybe (PageHeader msg) + , page : Html msg + , modal : Maybe (Html msg) + } + + + +-- CREATE + + +appDocument : String -> String -> AppHeader -> Html msg -> AppDocument msg +appDocument pageId title appHeader page = + { pageId = pageId + , title = title + , appHeader = appHeader + , pageHeader = Nothing + , page = page + , modal = Nothing + } + + + +-- MODIFY + + +withModal : Html msg -> AppDocument msg -> AppDocument msg +withModal modal appDoc = + { appDoc | modal = Just modal } + + + +-- MAP + + +map : (msgA -> msgB) -> AppDocument msgA -> AppDocument msgB +map toMsgB { pageId, title, appHeader, pageHeader, page, modal } = + { pageId = pageId + , title = title + , appHeader = appHeader + , pageHeader = Maybe.map (PageHeader.map toMsgB) pageHeader + , page = Html.map toMsgB page + , modal = Maybe.map (Html.map toMsgB) modal + } + + + +-- VIEW +{- viewAnnouncement + + Example when enabled: + + Just ( + div [ id "announcement" ] + [ div [ class "announcement_content" ] + [ text "🎉☁️ " + , Link.view "Unison Cloud" Link.unisonCloudWebsite + , text " is generally available!" + ] + ] + ) +-} + + +viewAnnouncement : Maybe (Html msg) +viewAnnouncement = + Nothing + + +view : AppHeaderContext msg -> AppDocument msg -> Document msg +view appHeaderCtx { pageId, title, appHeader, pageHeader, page, modal } = + { title = title ++ " | Unison Share" + , body = + [ div + [ id "app", class pageId ] + [ Maybe.withDefault UI.nothing viewAnnouncement + , AppHeader.view appHeaderCtx appHeader + , MaybeE.unwrap UI.nothing PageHeader.view pageHeader + , page + , Maybe.withDefault UI.nothing modal + ] + ] + } diff --git a/src/UnisonShare/AppError.elm b/src/UnisonShare/AppError.elm new file mode 100644 index 00000000..986fecf2 --- /dev/null +++ b/src/UnisonShare/AppError.elm @@ -0,0 +1,36 @@ +module UnisonShare.AppError exposing (..) + + +type AppError + = UnspecifiedError + | AccountCreationGitHubPermissionsRejected + | AccountCreationHandleAlreadyTaken + + +fromString : String -> AppError +fromString s = + case s of + "AccountCreationGitHubPermissionsRejected" -> + AccountCreationGitHubPermissionsRejected + + "AccountCreationHandleAlreadyTaken" -> + AccountCreationHandleAlreadyTaken + + "UnspecifiedError" -> + UnspecifiedError + + _ -> + UnspecifiedError + + +toString : AppError -> String +toString e = + case e of + AccountCreationGitHubPermissionsRejected -> + "AccountCreationGitHubPermissionsRejected" + + AccountCreationHandleAlreadyTaken -> + "AccountCreationHandleAlreadyTaken" + + UnspecifiedError -> + "UnspecifiedError" diff --git a/src/UnisonShare/AppHeader.elm b/src/UnisonShare/AppHeader.elm new file mode 100644 index 00000000..3f8984af --- /dev/null +++ b/src/UnisonShare/AppHeader.elm @@ -0,0 +1,331 @@ +module UnisonShare.AppHeader exposing (..) + +import Html exposing (Html, div, h1, span, text) +import Html.Attributes exposing (class, classList) +import Lib.HttpApi exposing (HttpApi) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Time +import UI.ActionMenu as ActionMenu +import UI.AnchoredOverlay as AnchoredOverlay +import UI.AppHeader exposing (AppHeader, AppTitle(..)) +import UI.Avatar as Avatar +import UI.Button as Button +import UI.Click as Click exposing (Click) +import UI.DateTime as DateTime +import UI.Icon as Icon +import UI.Navigation as Navigation exposing (NavItem) +import UI.Nudge as Nudge +import UI.Sizing as Sizing +import UI.ViewMode as ViewMode exposing (ViewMode) +import UnisonShare.Link as Link +import UnisonShare.Session exposing (Session(..)) +import Url exposing (Url) +import WhatsNew exposing (WhatsNew) + + +type AppHeader + = Disabled ViewMode + | AppHeader + { activeNavItem : ActiveNavItem + , viewMode : ViewMode + } + + +type ActiveNavItem + = None + | Catalog + | Profile + + + +-- CREATE + + +empty : AppHeader +empty = + Disabled ViewMode.Regular + + +appHeader : ActiveNavItem -> AppHeader +appHeader activeNavItem = + empty |> withActiveNavItem activeNavItem + + + +-- MODIFY + + +withViewMode : ViewMode -> AppHeader -> AppHeader +withViewMode vm appHeader_ = + case appHeader_ of + Disabled _ -> + Disabled vm + + AppHeader a -> + AppHeader { a | viewMode = vm } + + +withActiveNavItem : ActiveNavItem -> AppHeader -> AppHeader +withActiveNavItem activeNavItem appHeader_ = + case appHeader_ of + Disabled vm -> + AppHeader + { activeNavItem = activeNavItem + , viewMode = vm + } + + AppHeader a -> + AppHeader { a | activeNavItem = activeNavItem } + + + +-- VIEW + + +appTitle : Click msg -> AppTitle msg +appTitle click = + AppTitle click + (h1 [] + [ text "Unison" + , span [ class "context unison-share" ] [ text "Share" ] + ] + ) + + +navItems : { catalog : NavItem msg, profile : UserHandle -> NavItem msg } +navItems = + { catalog = Navigation.navItem "Catalog" (Click.href "/") + , profile = + \h -> + Navigation.navItem (UserHandle.toString h) (Link.userProfile h) + } + + +{-| Represents app level context, that is injected at render time +-} +type OpenedAppHeaderMenu + = NoneOpened + | HelpAndResourcesMenu + | AccountMenu + | CreateAccountMenu + + +type alias AppHeaderContext msg = + { session : Session + , timeZone : Time.Zone + , currentUrl : Url + , api : HttpApi + , openedAppHeaderMenu : OpenedAppHeaderMenu + , whatsNew : WhatsNew + , toggleHelpAndResourcesMenuMsg : msg + , toggleAccountMenuMsg : msg + , toggleCreateAccountMenuMsg : msg + , showKeyboardShortcutsModalMsg : msg + } + + +isHelpAndResourcesMenuOpen : OpenedAppHeaderMenu -> Bool +isHelpAndResourcesMenuOpen openedAppHeaderMenu = + openedAppHeaderMenu == HelpAndResourcesMenu + + +isAccountMenuOpen : OpenedAppHeaderMenu -> Bool +isAccountMenuOpen openedAppHeaderMenu = + openedAppHeaderMenu == AccountMenu + + +isCreateAccountMenuOpen : OpenedAppHeaderMenu -> Bool +isCreateAccountMenuOpen openedAppHeaderMenu = + openedAppHeaderMenu == CreateAccountMenu + + +view : AppHeaderContext msg -> AppHeader -> Html msg +view ctx appHeader_ = + case appHeader_ of + Disabled viewMode -> + viewBlank viewMode + + AppHeader { activeNavItem, viewMode } -> + let + whatsNewItemsLoading = + [ ActionMenu.loadingItem + , ActionMenu.loadingItem + , ActionMenu.loadingItem + , ActionMenu.loadingItem + ] + + whatsNewItems = + case ctx.whatsNew of + WhatsNew.Loading _ -> + whatsNewItemsLoading + + WhatsNew.Success wn -> + let + viewPost p = + let + postNudge = + if WhatsNew.isUnread wn p then + Nudge.nudge + + else + Nudge.empty + in + ActionMenu.optionItem_ + Nothing + p.title + (ActionMenu.DateTimeSubtext DateTime.ShortDate ctx.timeZone p.publishedAt) + postNudge + (Link.link p.url) + in + (wn.posts |> List.take 3 |> List.map viewPost) + ++ [ ActionMenu.optionItemWithoutIcon "View more..." Link.whatsNew + ] + + WhatsNew.Failure _ -> + [] + + nudge = + if WhatsNew.hasAnyUnreadPosts ctx.whatsNew then + Nudge.nudge + + else + Nudge.empty + + helpAndResources = + ActionMenu.items + (ActionMenu.titleItem "Whats new?") + (whatsNewItems + ++ [ ActionMenu.dividerItem + , ActionMenu.optionItem Icon.docs "Docs" Link.docs + , ActionMenu.optionItem Icon.bug "Report a Bug" Link.reportBug + , ActionMenu.optionItem Icon.keyboardKey "Keyboard Shortcuts" (Click.onClick ctx.showKeyboardShortcutsModalMsg) + , ActionMenu.optionItem Icon.unfoldedMap "Code of Conduct" Link.codeOfConduct + , ActionMenu.optionItem Icon.unisonMark "Unison Website" Link.website + , ActionMenu.optionItem Icon.github "Unison on GitHub" Link.github + ] + ) + |> ActionMenu.fromButton ctx.toggleHelpAndResourcesMenuMsg "Help & Resources" + |> ActionMenu.shouldBeOpen (isHelpAndResourcesMenuOpen ctx.openedAppHeaderMenu) + |> ActionMenu.withButtonIcon Icon.questionmark + |> ActionMenu.withNudge nudge + |> ActionMenu.withMaxWidth (Sizing.Rem 15) + |> ActionMenu.view + |> (\hr -> div [ class "help-and-resources" ] [ hr ]) + + loginLink = + Link.login ctx.api + + ( navigation, rightSide ) = + case ctx.session of + Anonymous -> + let + signIn = + Button.button_ (loginLink ctx.currentUrl) "Sign In" + |> Button.small + |> Button.emphasized + |> Button.view + + createAccountSheet = + AnchoredOverlay.sheet + (div [ class "create-account-menu" ] + [ Button.iconThenLabel_ (loginLink ctx.currentUrl) Icon.github "Connect with GitHub" + |> Button.medium + |> Button.emphasized + |> Button.view + , div [ class "terms-of-service" ] + [ text "By creating a Unison Account you agree to our " + , Link.view "Terms of Service" Link.termsOfService + , text ". Also see our " + , Link.view "Code of Conduct" Link.codeOfConduct + , text " and our " + , Link.view "Privacy Policy" Link.privacyPolicy + , text "." + ] + ] + ) + + createAccount = + let + createAccountIcon = + if isCreateAccountMenuOpen ctx.openedAppHeaderMenu then + Icon.caretUp + + else + Icon.caretDown + in + AnchoredOverlay.anchoredOverlay ctx.toggleCreateAccountMenuMsg + (Button.labelThenIcon ctx.toggleCreateAccountMenuMsg "Create Account" createAccountIcon + |> Button.small + |> Button.positive + |> Button.view + ) + |> AnchoredOverlay.withSheet_ (isCreateAccountMenuOpen ctx.openedAppHeaderMenu) createAccountSheet + |> AnchoredOverlay.view + + nav = + case activeNavItem of + Catalog -> + Navigation.empty |> Navigation.withItems [] navItems.catalog [] + + _ -> + Navigation.empty |> Navigation.withNoSelectedItems [ navItems.catalog ] + in + ( nav + , [ div [ class "sign-in-nav" ] [ helpAndResources, signIn, createAccount ] + , div [ class "sign-in-nav sign-in-nav_mobile" ] [ signIn, createAccount ] + ] + ) + + SignedIn sesh -> + let + nav = + case activeNavItem of + Catalog -> + Navigation.empty |> Navigation.withItems [] navItems.catalog [ navItems.profile sesh.handle ] + + Profile -> + Navigation.empty |> Navigation.withItems [ navItems.catalog ] (navItems.profile sesh.handle) [] + + _ -> + Navigation.empty |> Navigation.withNoSelectedItems [ navItems.catalog, navItems.profile sesh.handle ] + + avatar = + Avatar.avatar sesh.avatarUrl (Maybe.map (String.left 1) sesh.name) + |> Avatar.view + + viewAccountMenuTrigger isOpen = + let + chevron = + if isOpen then + Icon.chevronUp + + else + Icon.chevronDown + in + div [ classList [ ( "account-menu-trigger", True ), ( "account-menu_is-open", isOpen ) ] ] + [ avatar, Icon.view chevron ] + + accountMenu = + ActionMenu.items + (ActionMenu.optionItem Icon.cog "Account Settings" Link.account) + [ ActionMenu.optionItem Icon.exitDoor "Sign Out" (Link.logout ctx.api ctx.currentUrl) ] + |> ActionMenu.fromCustom ctx.toggleAccountMenuMsg viewAccountMenuTrigger + |> ActionMenu.shouldBeOpen (isAccountMenuOpen ctx.openedAppHeaderMenu) + |> ActionMenu.view + |> (\a -> div [ class "account-menu" ] [ a ]) + in + ( nav, [ helpAndResources, accountMenu ] ) + in + UI.AppHeader.appHeader (appTitle (Click.href "/")) + |> UI.AppHeader.withNavigation navigation + |> UI.AppHeader.withRightSide rightSide + |> UI.AppHeader.withViewMode viewMode + |> UI.AppHeader.view + + +viewBlank : ViewMode -> Html msg +viewBlank viewMode = + appTitle Click.Disabled + |> UI.AppHeader.appHeader + |> UI.AppHeader.withViewMode viewMode + |> UI.AppHeader.view diff --git a/src/UnisonShare/BranchSummary.elm b/src/UnisonShare/BranchSummary.elm new file mode 100644 index 00000000..f4641cc8 --- /dev/null +++ b/src/UnisonShare/BranchSummary.elm @@ -0,0 +1,14 @@ +module UnisonShare.BranchSummary exposing (BranchSummary, decode) + +import Code.Branch as Branch +import Json.Decode as Decode +import UnisonShare.Project as Project exposing (Project) + + +type alias BranchSummary = + Branch.BranchSummary (Project {}) + + +decode : Decode.Decoder BranchSummary +decode = + Branch.decodeSummary Project.decode diff --git a/src/UnisonShare/Catalog.elm b/src/UnisonShare/Catalog.elm new file mode 100644 index 00000000..d8146372 --- /dev/null +++ b/src/UnisonShare/Catalog.elm @@ -0,0 +1,88 @@ +module UnisonShare.Catalog exposing (..) + +import Json.Decode as Decode exposing (field, string) +import OrderedDict exposing (OrderedDict) +import UnisonShare.Project as Project exposing (ProjectSummary) +import UnisonShare.Project.ProjectRef as ProjectRef + + +type Catalog + = Catalog (OrderedDict String (List ProjectSummary)) + + +type alias CatalogWithFeatured = + { featured : Maybe (List ProjectSummary) + , rest : List ( String, List ProjectSummary ) + } + + + +-- HELPERS + + +{-| Create a Catalog from a list of grouped projects +-} +fromList : List ( String, List ProjectSummary ) -> Catalog +fromList groups = + Catalog (OrderedDict.fromList groups) + + +{-| Extract all categories from a Catalog +-} +categories : Catalog -> List String +categories (Catalog dict) = + OrderedDict.keys dict + + +{-| Extract all project listings from a Catalog +-} +projectListings : Catalog -> List ProjectSummary +projectListings (Catalog dict) = + List.concat (OrderedDict.values dict) + + +{-| Convert a Catalog to a list of project listings grouped by category +-} +toList : Catalog -> List ( String, List ProjectSummary ) +toList (Catalog dict) = + OrderedDict.toList dict + + +{-| Separate the "Featured" category out from the rest of the catalog entries +-} +asFeatured : Catalog -> CatalogWithFeatured +asFeatured catalog = + let + featured__ ( category, projects ) ( f, categories_ ) = + if String.toLower category == "featured" then + ( Just projects, categories_ ) + + else + ( f, categories_ ++ [ ( category, projects ) ] ) + + ( featured_, rest ) = + catalog + |> toList + |> List.foldl featured__ ( Nothing, [] ) + in + { featured = featured_, rest = rest } + + + +-- DECODE + + +decode : Decode.Decoder Catalog +decode = + let + decodeGroup = + Decode.map2 Tuple.pair + (field "name" string) + (field "projects" + (Decode.map + (List.sortBy (.ref >> ProjectRef.toString)) + (Decode.list Project.decodeSummary) + ) + ) + in + Decode.map fromList (Decode.list decodeGroup) diff --git a/src/UnisonShare/CodeBrowsingContext.elm b/src/UnisonShare/CodeBrowsingContext.elm new file mode 100644 index 00000000..092d6c55 --- /dev/null +++ b/src/UnisonShare/CodeBrowsingContext.elm @@ -0,0 +1,33 @@ +module UnisonShare.CodeBrowsingContext exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) + + +type CodeBrowsingContext + = UserCode UserHandle + | ProjectBranch ProjectRef BranchRef + + +user : UserHandle -> CodeBrowsingContext +user = + UserCode + + +project : ProjectRef -> BranchRef -> CodeBrowsingContext +project = + ProjectBranch + + +equals : CodeBrowsingContext -> CodeBrowsingContext -> Bool +equals a b = + case ( a, b ) of + ( UserCode handleA, UserCode handleB ) -> + UserHandle.equals handleA handleB + + ( ProjectBranch projectRefA branchRefA, ProjectBranch projectRefB branchRefB ) -> + ProjectRef.equals projectRefA projectRefB && BranchRef.equals branchRefA branchRefB + + _ -> + False diff --git a/src/UnisonShare/CodebaseStatus.elm b/src/UnisonShare/CodebaseStatus.elm new file mode 100644 index 00000000..5b14d36b --- /dev/null +++ b/src/UnisonShare/CodebaseStatus.elm @@ -0,0 +1,44 @@ +module UnisonShare.CodebaseStatus exposing (..) + +import Code.Perspective as Perspective +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.CodeBrowsingContext exposing (CodeBrowsingContext) + + +type CodebaseStatus + = Empty + | NotEmpty + + +fromIsEmpty : Bool -> CodebaseStatus +fromIsEmpty isEmpty_ = + if isEmpty_ then + Empty + + else + NotEmpty + + +isEmpty : CodebaseStatus -> Bool +isEmpty status = + status == Empty + + +checkStatus : (HttpResult CodebaseStatus -> msg) -> AppContext -> CodeBrowsingContext -> Cmd msg +checkStatus msg appContext context = + let + decoder = + -- Parse to "not-empty" since we don't actually care about the values, just that they exist + Decode.map (List.isEmpty >> fromIsEmpty) + (Decode.field "namespaceListingChildren" + (Decode.list (Decode.succeed "not-empty")) + ) + in + ShareApi.browseCodebase context + Perspective.relativeRootPerspective + Nothing + |> HttpApi.toRequest decoder msg + |> HttpApi.perform appContext.api diff --git a/src/UnisonShare/Contribution.elm b/src/UnisonShare/Contribution.elm new file mode 100644 index 00000000..885fb6f3 --- /dev/null +++ b/src/UnisonShare/Contribution.elm @@ -0,0 +1,58 @@ +module UnisonShare.Contribution exposing + ( Contribution + , dateOfHistoricDiffSupport + , decode + ) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Json.Decode as Decode +import Json.Decode.Pipeline exposing (optional, required) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import UI.DateTime as DateTime exposing (DateTime) +import UnisonShare.Contribution.ContributionRef as ContributionRef exposing (ContributionRef) +import UnisonShare.Contribution.ContributionStatus as ContributionStatus exposing (ContributionStatus) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) + + +type alias Contribution = + { ref : ContributionRef + , authorHandle : Maybe UserHandle + , sourceBranchRef : BranchRef + , targetBranchRef : BranchRef + , projectRef : ProjectRef + , createdAt : DateTime + , updatedAt : DateTime + , status : ContributionStatus + , numComments : Int + , title : String + , description : Maybe String + } + + + +-- HELPERS + + +dateOfHistoricDiffSupport : DateTime +dateOfHistoricDiffSupport = + DateTime.unsafeFromISO8601 "2024-04-30T00:00:00.001Z" + + + +-- DECODE + + +decode : Decode.Decoder Contribution +decode = + Decode.succeed Contribution + |> required "number" ContributionRef.decode + |> optional "author" (Decode.map Just UserHandle.decodeUnprefixed) Nothing + |> required "sourceBranchRef" BranchRef.decode + |> required "targetBranchRef" BranchRef.decode + |> required "projectRef" ProjectRef.decode + |> required "createdAt" DateTime.decode + |> required "updatedAt" DateTime.decode + |> required "status" ContributionStatus.decode + |> required "numComments" Decode.int + |> required "title" Decode.string + |> optional "description" (Decode.map Just Decode.string) Nothing diff --git a/src/UnisonShare/Contribution/ContributionEvent.elm b/src/UnisonShare/Contribution/ContributionEvent.elm new file mode 100644 index 00000000..ec784f4a --- /dev/null +++ b/src/UnisonShare/Contribution/ContributionEvent.elm @@ -0,0 +1,52 @@ +module UnisonShare.Contribution.ContributionEvent exposing (..) + +import Json.Decode as Decode +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (optional, required) +import UI.DateTime as DateTime +import UnisonShare.Contribution.ContributionStatus as ContributionStatus exposing (ContributionStatus) +import UnisonShare.Timeline.CommentEvent as CommentEvent exposing (CommentDetails, RemovedCommentDetails) +import UnisonShare.Timeline.TimelineEvent exposing (TimelineEventDetails) +import UnisonShare.User as User + + +type ContributionEvent + = StatusChange StatusChangeDetails + | Comment CommentDetails + | CommentRemoved RemovedCommentDetails + + +type alias StatusChangeDetails = + TimelineEventDetails + { newStatus : ContributionStatus + + -- TODO: Better support the initial change, which currently is implicitly + -- implied by this Maybe + , oldStatus : Maybe ContributionStatus + } + + +decodeStatusChangeDetails : Decode.Decoder StatusChangeDetails +decodeStatusChangeDetails = + let + makeStatusChangeDetails newStatus oldStatus timestamp actor = + { newStatus = newStatus + , oldStatus = oldStatus + , timestamp = timestamp + , actor = actor + } + in + Decode.succeed makeStatusChangeDetails + |> required "newStatus" ContributionStatus.decode + |> optional "oldStatus" (Decode.map Just ContributionStatus.decode) Nothing + |> required "timestamp" DateTime.decode + |> required "actor" User.decodeSummary + + +decode : Decode.Decoder ContributionEvent +decode = + Decode.oneOf + [ when (Decode.field "kind" Decode.string) ((==) "statusChange") (Decode.map StatusChange decodeStatusChangeDetails) + , when (Decode.field "kind" Decode.string) ((==) "comment") (Decode.map Comment CommentEvent.decodeCommentDetails) + , when (Decode.field "kind" Decode.string) ((==) "comment") (Decode.map CommentRemoved CommentEvent.decodeRemovedCommentDetails) + ] diff --git a/src/UnisonShare/Contribution/ContributionRef.elm b/src/UnisonShare/Contribution/ContributionRef.elm new file mode 100644 index 00000000..3c20159c --- /dev/null +++ b/src/UnisonShare/Contribution/ContributionRef.elm @@ -0,0 +1,96 @@ +module UnisonShare.Contribution.ContributionRef exposing + ( ContributionRef + , decode + , decodeString + , equals + , fromInt + , fromString + , fromUrl + , toApiString + , toString + , toUrlString + , unsafeFromString + ) + +import Json.Decode as Decode +import Lib.Util as Util +import Parser exposing (Parser) + + +type ContributionRef + = ContributionRef Int + + +fromInt : Int -> Maybe ContributionRef +fromInt n = + if n > 0 then + Just (ContributionRef n) + + else + Nothing + + +fromString : String -> Maybe ContributionRef +fromString s = + s + |> String.toInt + |> Maybe.andThen fromInt + + +unsafeFromString : String -> ContributionRef +unsafeFromString s = + fromString s + |> Maybe.withDefault (ContributionRef 0) + + +fromUrl : Parser ContributionRef +fromUrl = + let + parseMaybe mversion = + case mversion of + Just s_ -> + Parser.succeed s_ + + Nothing -> + Parser.problem "Invalid ContributionRef" + in + Parser.chompUntilEndOr "/" + |> Parser.getChompedString + |> Parser.map fromString + |> Parser.andThen parseMaybe + + +toString : ContributionRef -> String +toString (ContributionRef n) = + "#" ++ String.fromInt n + + +toUrlString : ContributionRef -> String +toUrlString (ContributionRef n) = + String.fromInt n + + +toApiString : ContributionRef -> String +toApiString (ContributionRef n) = + String.fromInt n + + +equals : ContributionRef -> ContributionRef -> Bool +equals (ContributionRef a) (ContributionRef b) = + a == b + + + +-- DECODE + + +decodeString : Decode.Decoder ContributionRef +decodeString = + Decode.map fromString Decode.string + |> Decode.andThen (Util.decodeFailInvalid "Invalid ContributionRef") + + +decode : Decode.Decoder ContributionRef +decode = + Decode.map fromInt Decode.int + |> Decode.andThen (Util.decodeFailInvalid "Invalid ContributionRef") diff --git a/src/UnisonShare/Contribution/ContributionStatus.elm b/src/UnisonShare/Contribution/ContributionStatus.elm new file mode 100644 index 00000000..0e487877 --- /dev/null +++ b/src/UnisonShare/Contribution/ContributionStatus.elm @@ -0,0 +1,45 @@ +module UnisonShare.Contribution.ContributionStatus exposing + ( ContributionStatus(..) + , decode + , toApiString + ) + +import Json.Decode as Decode +import Json.Decode.Extra exposing (when) + + +type ContributionStatus + = Draft + | InReview + | Merged + | Archived + + +toApiString : ContributionStatus -> String +toApiString status = + case status of + Draft -> + "draft" + + InReview -> + "in_review" + + Merged -> + "merged" + + Archived -> + "closed" + + + +-- DECODE + + +decode : Decode.Decoder ContributionStatus +decode = + Decode.oneOf + [ when Decode.string ((==) "draft") (Decode.succeed Draft) + , when Decode.string ((==) "in_review") (Decode.succeed InReview) + , when Decode.string ((==) "merged") (Decode.succeed Merged) + , when Decode.string ((==) "closed") (Decode.succeed Archived) + ] diff --git a/src/UnisonShare/ContributionTimeline.elm b/src/UnisonShare/ContributionTimeline.elm new file mode 100644 index 00000000..fc74832c --- /dev/null +++ b/src/UnisonShare/ContributionTimeline.elm @@ -0,0 +1,533 @@ +module UnisonShare.ContributionTimeline exposing (..) + +import Browser.Dom as Dom +import Dict exposing (Dict) +import Html exposing (Html, div) +import Html.Attributes exposing (class) +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import Task +import UI.Icon as Icon +import UI.Placeholder as Placeholder +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Contribution.ContributionEvent as ContributionEvent exposing (ContributionEvent(..)) +import UnisonShare.Contribution.ContributionRef exposing (ContributionRef) +import UnisonShare.Contribution.ContributionStatus exposing (ContributionStatus(..)) +import UnisonShare.DateTimeContext exposing (DateTimeContext) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Timeline.CommentEvent as CommentEvent exposing (CommentDetails) +import UnisonShare.Timeline.CommentId as CommentId exposing (CommentId) +import UnisonShare.Timeline.StatusChangeEvent as StatusChangeEvent + + +type alias ContributionTimeline = + List ContributionEvent + + +type alias ModifyCommentRequests = + Dict + -- This is a CommentId + String + { original : CommentDetails + , request : CommentEvent.ModifyCommentRequest + } + + +type alias Model = + { timeline : WebData ContributionTimeline + , modifyCommentRequests : ModifyCommentRequests + , newComment : CommentEvent.NewComment + } + + +init : AppContext -> ProjectRef -> ContributionRef -> ( Model, Cmd Msg ) +init appContext projectRef contribRef = + ( { timeline = Loading + , modifyCommentRequests = Dict.empty + , newComment = CommentEvent.WritingComment "" + } + , fetchContributionTimeline appContext projectRef contribRef + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchContributionTimelineFinished (WebData ContributionTimeline) + | UpdateNewComment String + | PostNewComment + | PostNewCommentFinished String (HttpResult CommentDetails) + | ResetNewComment + | ShowDeleteCommentConfirmation CommentId + | CancelDeleteComment CommentId + | DeleteComment CommentId + | DeleteCommentFinished CommentId (WebData ()) + | EditComment CommentId + | UpdateEditingComment CommentId String + | SaveEditComment CommentId String + | SaveEditCommentFinished CommentId String (HttpResult CommentDetails) + | CancelEditComment CommentId + + +update : AppContext -> ProjectRef -> ContributionRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef contribRef msg model = + case msg of + NoOp -> + ( model, Cmd.none ) + + FetchContributionTimelineFinished timeline -> + ( { model | timeline = timeline }, Cmd.none ) + + UpdateNewComment text -> + ( { model | newComment = CommentEvent.WritingComment text }, Cmd.none ) + + PostNewComment -> + case model.newComment of + CommentEvent.WritingComment text -> + if not (String.isEmpty text) then + ( { model | newComment = CommentEvent.PostingComment text } + , postContributionComment appContext projectRef contribRef text + ) + + else + ( model, Cmd.none ) + + CommentEvent.CommentPostingFailure text _ -> + if not (String.isEmpty text) then + ( { model | newComment = CommentEvent.PostingComment text } + , postContributionComment appContext projectRef contribRef text + ) + + else + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + PostNewCommentFinished _ (Ok comment) -> + let + commentEvent = + ContributionEvent.Comment comment + + timeline = + model.timeline + |> RemoteData.map (\l -> l ++ [ commentEvent ]) + in + ( { model | timeline = timeline, newComment = CommentEvent.CommentPostingSuccess comment } + , Util.delayMsg 2500 ResetNewComment + ) + + PostNewCommentFinished text (Err e) -> + ( { model | newComment = CommentEvent.CommentPostingFailure text e } + , Cmd.none + ) + + ResetNewComment -> + case model.newComment of + CommentEvent.CommentPostingSuccess _ -> + ( { model | newComment = CommentEvent.WritingComment "" }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + EditComment commentId -> + let + f evt acc = + case evt of + Comment details -> + if CommentId.equals commentId details.id then + Just details + + else + acc + + _ -> + acc + + original = + model.timeline + |> RemoteData.map (List.foldl f Nothing) + |> RemoteData.withDefault Nothing + in + case original of + Just orig -> + let + modifyCommentRequests = + Dict.insert + (CommentId.toString orig.id) + { original = orig + , request = CommentEvent.Editing orig.content + } + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Dom.focus ("edit_" ++ CommentId.toString orig.id) |> Task.attempt (always NoOp) + ) + + _ -> + ( model, Cmd.none ) + + UpdateEditingComment id edit -> + let + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.Editing edit })) + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + SaveEditComment id edit -> + case Dict.get (CommentId.toString id) model.modifyCommentRequests of + Just { original } -> + let + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.SavingEdit edit })) + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , updateContributionComment appContext projectRef contribRef id original.revision edit + ) + + Nothing -> + ( model, Cmd.none ) + + SaveEditCommentFinished id _ (Ok comment) -> + let + replaceComment evt = + case evt of + ContributionEvent.Comment c -> + if CommentId.equals c.id id then + ContributionEvent.Comment comment + + else + evt + + _ -> + evt + + timeline = + model.timeline + |> RemoteData.map (List.map replaceComment) + + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.EditSaved })) + model.modifyCommentRequests + in + ( { model | timeline = timeline, modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + SaveEditCommentFinished id _ (Err e) -> + let + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.EditFailure e })) + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + CancelEditComment id -> + let + modifyCommentRequests = + Dict.remove (CommentId.toString id) model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + ShowDeleteCommentConfirmation commentId -> + let + f evt acc = + case evt of + Comment details -> + if CommentId.equals commentId details.id then + Just details + + else + acc + + _ -> + Nothing + + original = + model.timeline + |> RemoteData.map (List.foldl f Nothing) + |> RemoteData.withDefault Nothing + in + case original of + Just orig -> + let + modifyCommentRequests = + Dict.insert + (CommentId.toString orig.id) + { original = orig + , request = CommentEvent.ConfirmDelete + } + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + _ -> + ( model, Cmd.none ) + + CancelDeleteComment id -> + let + modifyCommentRequests = + Dict.remove (CommentId.toString id) model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + DeleteComment commentId -> + let + removedDetails c = + { id = commentId + , timestamp = c.timestamp + , deletedAt = appContext.now + } + + eventToRemoved idToRemove evt ( evts, removed ) = + case evt of + Comment d -> + if CommentId.equals d.id idToRemove then + ( CommentRemoved (removedDetails d) :: evts, Just d ) + + else + ( evt :: evts, removed ) + + _ -> + ( evt :: evts, removed ) + + ( timeline, removedComment ) = + model.timeline + |> RemoteData.map (List.foldr (eventToRemoved commentId) ( [], Nothing )) + |> RemoteData.unwrap ( model.timeline, Nothing ) (\( t, r ) -> ( Success t, r )) + in + case removedComment of + Just c -> + let + modifyCommentRequests = + Dict.insert + (CommentId.toString commentId) + { original = c, request = CommentEvent.Deleting } + model.modifyCommentRequests + in + ( { model + | timeline = timeline + , modifyCommentRequests = modifyCommentRequests + } + , deleteContributionComment + appContext + projectRef + contribRef + commentId + ) + + Nothing -> + ( model, Cmd.none ) + + DeleteCommentFinished commentId res -> + let + timeline = + case ( res, Dict.get (CommentId.toString commentId) model.modifyCommentRequests ) of + ( Failure _, Just c ) -> + let + eventToRemoved evt = + case evt of + CommentRemoved d -> + if CommentId.equals d.id commentId then + Comment c.original + + else + evt + + _ -> + evt + in + model.timeline + |> RemoteData.map (List.map eventToRemoved) + + _ -> + model.timeline + + request r = + case res of + Success _ -> + CommentEvent.Deleted + + Failure err -> + CommentEvent.DeleteFailure err + + _ -> + r.request + + modifyCommentRequests = + Dict.update + (CommentId.toString commentId) + (Maybe.map (\r -> { r | request = request r })) + model.modifyCommentRequests + in + ( { model | timeline = timeline, modifyCommentRequests = modifyCommentRequests }, Cmd.none ) + + +addEvent : Model -> ContributionEvent -> Model +addEvent model event = + let + timeline = + model.timeline + |> RemoteData.map (\tl -> tl ++ [ event ]) + in + { model | timeline = timeline } + + +isUpdatable : Model -> Bool +isUpdatable model = + RemoteData.isSuccess model.timeline + + + +-- EFFECTS + + +fetchContributionTimeline : AppContext -> ProjectRef -> ContributionRef -> Cmd Msg +fetchContributionTimeline appContext projectRef contributionRef = + ShareApi.projectContributionTimeline projectRef contributionRef + |> HttpApi.toRequest + (Decode.field "items" (Decode.list ContributionEvent.decode)) + (RemoteData.fromResult >> FetchContributionTimelineFinished) + |> HttpApi.perform appContext.api + + +postContributionComment : AppContext -> ProjectRef -> ContributionRef -> String -> Cmd Msg +postContributionComment appContext projectRef contributionRef text = + ShareApi.createProjectContributionComment projectRef contributionRef text + |> HttpApi.toRequest CommentEvent.decodeCommentDetails (PostNewCommentFinished text) + |> HttpApi.perform appContext.api + + +updateContributionComment : AppContext -> ProjectRef -> ContributionRef -> CommentId -> Int -> String -> Cmd Msg +updateContributionComment appContext projectRef contributionRef commentId originalRevision text = + ShareApi.updateProjectContributionComment projectRef contributionRef commentId originalRevision text + |> HttpApi.toRequest (Decode.field "comment" CommentEvent.decodeCommentDetails) (SaveEditCommentFinished commentId text) + |> HttpApi.perform appContext.api + + +deleteContributionComment : AppContext -> ProjectRef -> ContributionRef -> CommentId -> Cmd Msg +deleteContributionComment appContext projectRef contributionRef commentId = + ShareApi.deleteProjectContributionComment projectRef contributionRef commentId + |> HttpApi.toRequestWithEmptyResponse + (RemoteData.fromResult >> DeleteCommentFinished commentId) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewStatusChangeEvent : DateTimeContext a -> ContributionEvent.StatusChangeDetails -> Html Msg +viewStatusChangeEvent dtContext ({ newStatus, oldStatus } as details) = + case newStatus of + Draft -> + StatusChangeEvent.view dtContext Icon.writingPad "Created Draft" details + + InReview -> + let + title = + case oldStatus of + Just Archived -> + "Re-opened" + + _ -> + "Submitted for review" + in + StatusChangeEvent.view dtContext Icon.conversation title details + + Merged -> + StatusChangeEvent.view dtContext Icon.merge "Merged" details + + Archived -> + StatusChangeEvent.view dtContext Icon.archive "Archived" details + + +viewContributionEvent : AppContext -> ProjectRef -> ModifyCommentRequests -> ContributionEvent -> Html Msg +viewContributionEvent appContext projectRef modifyCommentRequests event = + let + actions commenter = + CommentEvent.commentEventActions appContext + { editMsg = EditComment + , updateEditingMsg = UpdateEditingComment + , saveEditMsg = SaveEditComment + , cancelEditMsg = CancelEditComment + , deleteMsg = ShowDeleteCommentConfirmation + , confirmDeleteMsg = DeleteComment + , cancelDeleteMsg = CancelDeleteComment + } + projectRef + commenter + in + case event of + ContributionEvent.StatusChange details -> + div [ class "timeline-event" ] + [ viewStatusChangeEvent appContext details ] + + ContributionEvent.Comment details -> + let + request = + modifyCommentRequests + |> Dict.get (CommentId.toString details.id) + |> Maybe.map .request + |> Maybe.withDefault CommentEvent.Idle + in + div [ class "timeline-event" ] + [ CommentEvent.viewCommentEvent + appContext + (actions details.actor.handle) + { details = details, request = request } + ] + + ContributionEvent.CommentRemoved details -> + div [ class "timeline-event" ] + [ CommentEvent.viewRemovedCommentEvent + appContext + details + ] + + +view : AppContext -> ProjectRef -> Model -> Html Msg +view appContext projectRef model = + case model.timeline of + Success timeline -> + div [] + [ div [ class "timeline" ] (List.map (viewContributionEvent appContext projectRef model.modifyCommentRequests) timeline) + , CommentEvent.viewNewComment + appContext + { comment = model.newComment + , updateMsg = UpdateNewComment + , postMsg = PostNewComment + } + ] + + _ -> + div [] + [ Placeholder.text + |> Placeholder.view + ] diff --git a/src/UnisonShare/DateTimeContext.elm b/src/UnisonShare/DateTimeContext.elm new file mode 100644 index 00000000..4d1a0a8d --- /dev/null +++ b/src/UnisonShare/DateTimeContext.elm @@ -0,0 +1,11 @@ +module UnisonShare.DateTimeContext exposing (DateTimeContext) + +import Time +import UI.DateTime exposing (DateTime) + + +type alias DateTimeContext a = + { a + | now : DateTime + , timeZone : Time.Zone + } diff --git a/src/UnisonShare/Diff.elm b/src/UnisonShare/Diff.elm new file mode 100644 index 00000000..5462500c --- /dev/null +++ b/src/UnisonShare/Diff.elm @@ -0,0 +1,222 @@ +module UnisonShare.Diff exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.FullyQualifiedName as FQN exposing (FQN) +import Code.Hash as Hash exposing (Hash) +import Json.Decode as Decode exposing (Decoder, field, oneOf) +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (required, requiredAt) +import Lib.Util exposing (decodeNonEmptyList, decodeTag) +import List.Nonempty as NEL + + +type DefinitionDiff + = Added { hash : Hash, shortName : FQN, fullName : FQN } + | Removed { hash : Hash, shortName : FQN, fullName : FQN } + | Updated { oldHash : Hash, newHash : Hash, shortName : FQN, fullName : FQN } + | RenamedFrom { hash : Hash, oldNames : NEL.Nonempty FQN, newShortName : FQN, newFullName : FQN } + | Aliased { hash : Hash, aliasShortName : FQN, aliasFullName : FQN, otherNames : NEL.Nonempty FQN } + + +type DiffLine + = TermDiffLine DefinitionDiff + | TypeDiffLine DefinitionDiff + | AbilityDiffLine DefinitionDiff + | DocDiffLine DefinitionDiff + | TestDiffLine DefinitionDiff + | DataConstructorDiffLine DefinitionDiff + | AbilityConstructorDiffLine DefinitionDiff + | NamespaceDiffLine { name : FQN, lines : List DiffLine } + + +type alias DiffBranchRef = + { ref : BranchRef, hash : Hash } + + +type alias Diff = + { lines : List DiffLine + , oldBranch : DiffBranchRef + , newBranch : DiffBranchRef + } + + + +-- HELPERS + + +summary : List DiffLine -> { numChanges : Int, numNamespaceChanges : Int } +summary diffLines = + let + f diffLine acc = + case diffLine of + NamespaceDiffLine { lines } -> + let + nested = + summary lines + in + { numChanges = acc.numChanges + nested.numChanges + , numNamespaceChanges = acc.numNamespaceChanges + nested.numNamespaceChanges + 1 + } + + _ -> + { acc | numChanges = acc.numChanges + 1 } + in + List.foldl f { numChanges = 0, numNamespaceChanges = 0 } diffLines + + +sortDiffLines : List DiffLine -> List DiffLine +sortDiffLines lines = + let + sortKey diffLine = + let + name_ defDiff = + case defDiff of + Added { shortName } -> + shortName + + Removed { shortName } -> + shortName + + Updated { shortName } -> + shortName + + RenamedFrom { newShortName } -> + newShortName + + Aliased { aliasShortName } -> + aliasShortName + in + case diffLine of + TermDiffLine defDiff -> + name_ defDiff + + TypeDiffLine defDiff -> + name_ defDiff + + AbilityDiffLine defDiff -> + name_ defDiff + + DocDiffLine defDiff -> + name_ defDiff + + TestDiffLine defDiff -> + name_ defDiff + + DataConstructorDiffLine defDiff -> + name_ defDiff + + AbilityConstructorDiffLine defDiff -> + name_ defDiff + + NamespaceDiffLine ns -> + ns.name + in + List.sortBy (sortKey >> FQN.toString) lines + + + +-- DECODE + + +decodeDefinitionDiff : Decoder DefinitionDiff +decodeDefinitionDiff = + let + added_ hash shortName fullName = + Added { hash = hash, shortName = shortName, fullName = fullName } + + removed_ hash shortName fullName = + Removed { hash = hash, shortName = shortName, fullName = fullName } + + updated_ oldHash newHash shortName fullName = + Updated { oldHash = oldHash, newHash = newHash, shortName = shortName, fullName = fullName } + + renamedFrom_ hash oldNames newShortName newFullName = + RenamedFrom { hash = hash, oldNames = oldNames, newShortName = newShortName, newFullName = newFullName } + + aliased_ hash aliasShortName aliasFullName otherNames = + Aliased { hash = hash, aliasShortName = aliasShortName, otherNames = otherNames, aliasFullName = aliasFullName } + in + oneOf + [ when decodeTag + ((==) "Added") + (Decode.succeed added_ + |> requiredAt [ "contents", "hash" ] Hash.decode + |> requiredAt [ "contents", "shortName" ] FQN.decode + |> requiredAt [ "contents", "fullName" ] FQN.decode + ) + , when decodeTag + ((==) "Removed") + (Decode.succeed removed_ + |> requiredAt [ "contents", "hash" ] Hash.decode + |> requiredAt [ "contents", "shortName" ] FQN.decode + |> requiredAt [ "contents", "fullName" ] FQN.decode + ) + , when decodeTag + ((==) "Updated") + (Decode.succeed updated_ + |> requiredAt [ "contents", "oldHash" ] Hash.decode + |> requiredAt [ "contents", "newHash" ] Hash.decode + |> requiredAt [ "contents", "shortName" ] FQN.decode + |> requiredAt [ "contents", "fullName" ] FQN.decode + ) + , when decodeTag + ((==) "RenamedFrom") + (Decode.succeed renamedFrom_ + |> requiredAt [ "contents", "hash" ] Hash.decode + |> requiredAt [ "contents", "oldNames" ] (decodeNonEmptyList FQN.decode) + |> requiredAt [ "contents", "newShortName" ] FQN.decode + |> requiredAt [ "contents", "newFullName" ] FQN.decode + ) + , when decodeTag + ((==) "Aliased") + (Decode.succeed aliased_ + |> requiredAt [ "contents", "hash" ] Hash.decode + |> requiredAt [ "contents", "aliasShortName" ] FQN.decode + |> requiredAt [ "contents", "aliasFullName" ] FQN.decode + |> requiredAt [ "contents", "otherNames" ] (decodeNonEmptyList FQN.decode) + ) + ] + + +decodeDiffLine : Decoder DiffLine +decodeDiffLine = + oneOf + [ when decodeTag ((==) "Plain") (Decode.map TermDiffLine (field "contents" decodeDefinitionDiff)) + , when decodeTag ((==) "Data") (Decode.map TypeDiffLine (field "contents" decodeDefinitionDiff)) + , when decodeTag ((==) "Ability") (Decode.map AbilityDiffLine (field "contents" decodeDefinitionDiff)) + , when decodeTag ((==) "Doc") (Decode.map DocDiffLine (field "contents" decodeDefinitionDiff)) + , when decodeTag ((==) "Test") (Decode.map TestDiffLine (field "contents" decodeDefinitionDiff)) + , when decodeTag ((==) "DataConstructor") (Decode.map DataConstructorDiffLine (field "contents" decodeDefinitionDiff)) + , when decodeTag ((==) "AbilityConstructor") (Decode.map AbilityConstructorDiffLine (field "contents" decodeDefinitionDiff)) + ] + + +decodeNamespace : Decoder DiffLine +decodeNamespace = + let + makeNamespace name changes children = + NamespaceDiffLine { name = name, lines = changes ++ children } + in + Decode.succeed + makeNamespace + |> requiredAt [ "path" ] FQN.decode + |> requiredAt [ "contents", "changes" ] (Decode.list decodeDiffLine) + |> requiredAt [ "contents", "children" ] (Decode.list (Decode.lazy (\_ -> decodeNamespace))) + + +decode : Decoder Diff +decode = + let + mk oldRef oldRefHash newRef newRefHash changes children = + { oldBranch = { ref = oldRef, hash = oldRefHash } + , newBranch = { ref = newRef, hash = newRefHash } + , lines = changes ++ children + } + in + Decode.succeed mk + |> required "oldRef" BranchRef.decode + |> required "oldRefHash" Hash.decode + |> required "newRef" BranchRef.decode + |> required "newRefHash" Hash.decode + |> requiredAt [ "diff", "changes" ] (Decode.list decodeDiffLine) + |> requiredAt [ "diff", "children" ] (Decode.list decodeNamespace) diff --git a/src/UnisonShare/InteractiveDoc.elm b/src/UnisonShare/InteractiveDoc.elm new file mode 100644 index 00000000..c548f339 --- /dev/null +++ b/src/UnisonShare/InteractiveDoc.elm @@ -0,0 +1,82 @@ +--- A small stateful wrapper around Code.Definition.Doc + + +module UnisonShare.InteractiveDoc exposing (..) + +import Code.Config exposing (Config) +import Code.Definition.Doc as Doc exposing (Doc, DocFoldToggles) +import Code.Definition.Reference exposing (Reference) +import Code.DefinitionSummaryTooltip as DefinitionSummaryTooltip +import Code.Syntax as Syntax +import Html exposing (Html) +import UI.Click as Click + + + +-- MODEL + + +type alias Model = + { foldToggles : DocFoldToggles + , definitionSummaryTooltip : DefinitionSummaryTooltip.Model + } + + +init : Model +init = + { foldToggles = Doc.emptyDocFoldToggles + , definitionSummaryTooltip = DefinitionSummaryTooltip.init + } + + + +-- UPDATE + + +type Msg + = OpenReference Reference + | ToggleDocFold Doc.FoldId + | DefinitionSummaryTooltipMsg DefinitionSummaryTooltip.Msg + + +type OutMsg + = OpenDefinition Reference + | None + + +update : Config -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update config msg model = + case msg of + OpenReference r -> + ( model, Cmd.none, OpenDefinition r ) + + ToggleDocFold fid -> + ( { model | foldToggles = Doc.toggleFold model.foldToggles fid }, Cmd.none, None ) + + DefinitionSummaryTooltipMsg tMsg -> + let + ( definitionSummaryTooltip, tCmd ) = + DefinitionSummaryTooltip.update config tMsg model.definitionSummaryTooltip + in + ( { model | definitionSummaryTooltip = definitionSummaryTooltip } + , Cmd.map DefinitionSummaryTooltipMsg tCmd + , None + ) + + + +-- VIEW + + +view : Model -> Doc -> Html Msg +view model doc = + let + syntaxConfig = + Syntax.linkedWithTooltipConfig + (OpenReference >> Click.onClick) + (DefinitionSummaryTooltip.tooltipConfig + DefinitionSummaryTooltipMsg + model.definitionSummaryTooltip + ) + in + Doc.view syntaxConfig ToggleDocFold model.foldToggles doc diff --git a/src/UnisonShare/Link.elm b/src/UnisonShare/Link.elm new file mode 100644 index 00000000..2a82bfb3 --- /dev/null +++ b/src/UnisonShare/Link.elm @@ -0,0 +1,298 @@ +module UnisonShare.Link exposing (..) + +import Code.BranchRef exposing (BranchRef) +import Code.Definition.Reference exposing (Reference) +import Code.FullyQualifiedName exposing (FQN) +import Code.Perspective as Perspective exposing (Perspective) +import Code.Version exposing (Version) +import Html exposing (Html, text) +import Lib.HttpApi as HttpApi exposing (HttpApi) +import Lib.UserHandle exposing (UserHandle) +import UI.Click as Click exposing (Click) +import UnisonShare.Contribution.ContributionRef exposing (ContributionRef) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Route as Route exposing (Route) +import UnisonShare.Ticket.TicketRef exposing (TicketRef) +import Url exposing (Url) + + + +{- + + Link + ==== + + Various UI.Click link helpers for Routes and external links + +-} +-- EXTERNAL + + +link : String -> Click msg +link url = + Click.externalHref url + + +unisonCloudWebsite : Click msg +unisonCloudWebsite = + Click.externalHref "https://unison.cloud" + + +website : Click msg +website = + Click.externalHref "https://unison-lang.org" + + +whatsNew : Click msg +whatsNew = + Click.externalHref "https://unison-lang.org/whats-new" + + +whatsNewPost : String -> Click msg +whatsNewPost postPath = + Click.externalHref ("https://unison-lang.org/whats-new/" ++ postPath) + + +github : Click msg +github = + Click.externalHref "https://github.com/unisonweb/unison" + + +githubReleases : Click msg +githubReleases = + Click.externalHref "https://github.com/unisonweb/unison/releases" + + +githubRelease : String -> Click msg +githubRelease releaseTag = + Click.externalHref ("https://github.com/unisonweb/unison/releases/tag/" ++ releaseTag) + + +reportBug : Click msg +reportBug = + Click.externalHref "https://github.com/unisonweb/unison/issues/new?%5B%5Dlabels=unison-share" + + +docs : Click msg +docs = + Click.externalHref "https://unison-lang.org/docs" + + +unisonShareDocs : Click msg +unisonShareDocs = + Click.externalHref "https://unison-lang.org/learn/tooling/unison-share" + + +tour : Click msg +tour = + Click.externalHref "https://unison-lang.org/docs/tour" + + +codeOfConduct : Click msg +codeOfConduct = + Click.externalHref "https://www.unison-lang.org/community/code-of-conduct/" + + +status : Click msg +status = + Click.externalHref "https://unison.statuspage.io" + + +discord : Click msg +discord = + Click.externalHref "https://unison-lang.com/discord" + + +login : HttpApi -> Url -> Click msg +login api returnTo = + let + returnTo_ = + returnTo + |> Url.toString + |> Url.percentEncode + in + Click.externalHref_ Click.Self (HttpApi.baseApiUrl api ++ "login?return_to=" ++ returnTo_) + + +logout : HttpApi -> Url -> Click msg +logout api returnTo = + let + returnTo_ = + returnTo + |> Url.toString + |> Url.percentEncode + in + Click.externalHref_ Click.Self (HttpApi.baseApiUrl api ++ "logout?return_to=" ++ returnTo_) + + + +-- WITHIN SHARE + + +home : Click msg +home = + toClick Route.catalog + + +catalog : Click msg +catalog = + toClick Route.catalog + + +account : Click msg +account = + toClick Route.account + + +userProfile : UserHandle -> Click msg +userProfile = + Route.userProfile >> toClick + + +userDefinition : UserHandle -> Perspective -> Reference -> Click msg +userDefinition handle_ pers ref = + toClick (Route.userDefinition handle_ pers ref) + + +userCodeRoot : UserHandle -> Click msg +userCodeRoot handle_ = + let + pers = + Perspective.relativeRootPerspective + in + toClick (Route.userCodeRoot handle_ pers) + + +userNamespaceRoot : UserHandle -> FQN -> Click msg +userNamespaceRoot handle_ namespaceFqn = + let + pers = + Perspective.namespacePerspective namespaceFqn + in + toClick (Route.userCodeRoot handle_ pers) + + +userContributions : UserHandle -> Click msg +userContributions = + Route.userContributions >> toClick + + +projectOverview : ProjectRef -> Click msg +projectOverview projectRef_ = + toClick (Route.projectOverview projectRef_) + + +projectBranches : ProjectRef -> Click msg +projectBranches projectRef_ = + toClick (Route.projectBranches projectRef_) + + +projectBranchDefinition : ProjectRef -> BranchRef -> Perspective -> Reference -> Click msg +projectBranchDefinition projectRef_ branchRef pers ref = + toClick (Route.projectBranchDefinition projectRef_ branchRef pers ref) + + +projectBranchRoot : ProjectRef -> BranchRef -> Click msg +projectBranchRoot projectRef_ branchRef = + let + pers = + Perspective.relativeRootPerspective + in + toClick (Route.projectBranchRoot projectRef_ branchRef pers) + + +projectNamespaceRoot : ProjectRef -> BranchRef -> FQN -> Click msg +projectNamespaceRoot projectRef_ branchRef namespaceFqn = + let + pers = + Perspective.namespacePerspective namespaceFqn + in + toClick (Route.projectBranchRoot projectRef_ branchRef pers) + + +projectRelease : ProjectRef -> Version -> Click msg +projectRelease projectRef_ version = + toClick (Route.projectRelease projectRef_ version) + + +projectReleases : ProjectRef -> Click msg +projectReleases projectRef_ = + toClick (Route.projectReleases projectRef_) + + +projectContribution : ProjectRef -> ContributionRef -> Click msg +projectContribution projectRef_ contribRef = + toClick (Route.projectContribution projectRef_ contribRef) + + +projectContributionOverview : ProjectRef -> ContributionRef -> Click msg +projectContributionOverview projectRef_ contribRef = + toClick (Route.projectContribution projectRef_ contribRef) + + +projectContributionChanges : ProjectRef -> ContributionRef -> Click msg +projectContributionChanges projectRef_ contribRef = + toClick (Route.projectContributionChanges projectRef_ contribRef) + + +projectContributions : ProjectRef -> Click msg +projectContributions projectRef_ = + toClick (Route.projectContributions projectRef_) + + +projectTicket : ProjectRef -> TicketRef -> Click msg +projectTicket projectRef_ ticketRef = + toClick (Route.projectTicket projectRef_ ticketRef) + + +projectTickets : ProjectRef -> Click msg +projectTickets projectRef_ = + toClick (Route.projectTickets projectRef_) + + +projectSettings : ProjectRef -> Click msg +projectSettings projectRef_ = + toClick (Route.projectSettings projectRef_) + + +termsOfService : Click msg +termsOfService = + toClick Route.termsOfService + + +privacyPolicy : Click msg +privacyPolicy = + toClick Route.privacyPolicy + + +ucmConnected : Click msg +ucmConnected = + toClick Route.ucmConnected + + +cloud : Click msg +cloud = + toClick Route.cloud + + + +-- VIEW + + +view : String -> Click msg -> Html msg +view label click = + view_ (text label) click + + +view_ : Html msg -> Click msg -> Html msg +view_ label_ click = + Click.view [] [ label_ ] click + + + +-- HELPERS + + +toClick : Route -> Click msg +toClick = + Route.toUrlString >> Click.href diff --git a/src/UnisonShare/Log.elm b/src/UnisonShare/Log.elm new file mode 100644 index 00000000..fdfdd177 --- /dev/null +++ b/src/UnisonShare/Log.elm @@ -0,0 +1,21 @@ +module UnisonShare.Log exposing (..) + + +type LogEntryStatus + = Success + | Info + | Error + + +type alias LogEntry l = + { l + | status : LogEntryStatus + , id : String + , title : String + , description : Maybe String + , loggedAt : String + } + + +type alias Log a = + List (LogEntry a) diff --git a/src/UnisonShare/Metrics.elm b/src/UnisonShare/Metrics.elm new file mode 100644 index 00000000..d6e53beb --- /dev/null +++ b/src/UnisonShare/Metrics.elm @@ -0,0 +1,27 @@ +port module UnisonShare.Metrics exposing + ( Event(..) + , track + ) + +import Json.Encode as Encode exposing (object, string) + + +type Event + = View String + + +track : Event -> Cmd msg +track evt = + evt |> encode |> trackEvent + + +encode : Event -> Encode.Value +encode ev = + case ev of + View thing -> + object + [ ( "event", string ("view" ++ thing) ) + ] + + +port trackEvent : Encode.Value -> Cmd msg diff --git a/src/UnisonShare/Page/AcceptTermsPage.elm b/src/UnisonShare/Page/AcceptTermsPage.elm new file mode 100644 index 00000000..050a7981 --- /dev/null +++ b/src/UnisonShare/Page/AcceptTermsPage.elm @@ -0,0 +1,143 @@ +{- This page is used as an interstitial for users signing via UCM or Cloud -} + + +module UnisonShare.Page.AcceptTermsPage exposing (..) + +import Browser.Navigation exposing (load) +import Html exposing (div) +import Html.Attributes exposing (class) +import Lib.HttpApi as HttpApi +import Lib.Util as Util +import Markdown +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.Card as Card +import UI.Icon as Icon +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.PageTitle as PageTitle +import UI.StatusBanner as StatusBanner +import UI.StatusIndicator as StatusIndicator +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Tour as Tour +import Url exposing (Url) + + + +-- MODEL + + +type alias Model = + WebData () + + +init : Model +init = + NotAsked + + + +-- UPDATE + + +type Msg + = AcceptTerms + | AcceptTermsFinished (WebData ()) + | Redirect + + +update : AppContext -> Msg -> Maybe Url -> Model -> ( Model, Cmd Msg ) +update appContext msg continueUrl model = + case msg of + AcceptTerms -> + ( model, acceptTerms appContext ) + + AcceptTermsFinished r -> + ( r, Util.delayMsg 1000 Redirect ) + + Redirect -> + case continueUrl of + Just url -> + ( model, load (Url.toString url) ) + + Nothing -> + -- TODO: Maybe show a message indicating they can go back to ucm (or cloud, or where ever they came from)? + ( model, Cmd.none ) + + + +-- EFFECTS + + +acceptTerms : AppContext -> Cmd Msg +acceptTerms appContext = + ShareApi.completeTours [ Tour.WelcomeTerms ] + |> HttpApi.toRequestWithEmptyResponse + (RemoteData.fromResult >> AcceptTermsFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +view : Model -> AppDocument Msg +view model = + let + acceptButton = + Button.iconThenLabel AcceptTerms Icon.checkmark "Accept Terms of Service and continue" + |> Button.positive + |> Button.medium + |> Button.view + + ( status, acceptButton_ ) = + case model of + NotAsked -> + ( UI.nothing, acceptButton ) + + Loading -> + ( StatusIndicator.view StatusIndicator.working, acceptButton ) + + Success _ -> + ( StatusBanner.good "Successfully accepted terms, redirecting...", UI.nothing ) + + Failure _ -> + ( StatusBanner.bad "Something broke on our end and we couldn't accept the terms, please try again", acceptButton ) + + content = + Card.card + [ div [ class "definition-doc" ] + [ Markdown.toHtml [] "require:src/terms-of-service.md" ] + ] + |> Card.asContained + |> Card.view + + footer = + div [ class "accept" ] + [ div [ class "status" ] [ status ] + , acceptButton_ + ] + + page = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ content, footer ] + |> PageContent.withPageTitle + (PageTitle.title "Accept the Unison Terms of Service to continue" + |> PageTitle.withIcon Icon.documentCertificate + ) + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + in + { pageId = "accept-terms-page" + , title = "Unison Terms of Service" + , appHeader = AppHeader.empty + , pageHeader = Nothing + , page = PageLayout.view page + , modal = Nothing + } diff --git a/src/UnisonShare/Page/AccountPage.elm b/src/UnisonShare/Page/AccountPage.elm new file mode 100644 index 00000000..f3299868 --- /dev/null +++ b/src/UnisonShare/Page/AccountPage.elm @@ -0,0 +1,261 @@ +module UnisonShare.Page.AccountPage exposing + ( Model(..) + , Msg(..) + , init + , update + , view + ) + +import Html exposing (Html, div, em, p, section, strong, text) +import Html.Attributes exposing (class) +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.UserHandle as UserHandle +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.Card as Card +import UI.Click as Click +import UI.Icon as Icon +import UI.Modal as Modal +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.PageTitle as PageTitle +import UI.StatusBanner as StatusBanner +import UnisonShare.Account exposing (Account) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.PageFooter as PageFooter + + + +-- MODEL + + +type Confirm + = NotConfirmed + | Confirmed (WebData ()) + + +type Model + = NoModal + | ExportDataModal Confirm + | DeleteAccountModal Confirm + + +init : Model +init = + NoModal + + + +-- MSG + + +type Msg + = ShowExportDataModal + | ExportDataConfirm + | ExportDataRequestFinished (HttpResult ()) + | ShowDeleteAccountModal + | DeleteAccountConfirm + | DeleteAccountRequestFinished (HttpResult ()) + | CloseModal + + +update : AppContext -> Account a -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext _ msg _ = + case msg of + ShowExportDataModal -> + ( ExportDataModal NotConfirmed, Cmd.none ) + + ExportDataConfirm -> + ( ExportDataModal (Confirmed Loading), exportDataRequest appContext ) + + ExportDataRequestFinished (Ok r) -> + ( ExportDataModal (Confirmed (Success r)), Cmd.none ) + + ExportDataRequestFinished (Err e) -> + ( ExportDataModal (Confirmed (Failure e)), Cmd.none ) + + ShowDeleteAccountModal -> + ( DeleteAccountModal NotConfirmed, Cmd.none ) + + DeleteAccountConfirm -> + ( DeleteAccountModal (Confirmed Loading), deleteAccountRequest appContext ) + + DeleteAccountRequestFinished (Ok r) -> + ( DeleteAccountModal (Confirmed (Success r)), Cmd.none ) + + DeleteAccountRequestFinished (Err e) -> + ( DeleteAccountModal (Confirmed (Failure e)), Cmd.none ) + + CloseModal -> + ( NoModal, Cmd.none ) + + + +-- EFFECTS + + +exportDataRequest : AppContext -> Cmd Msg +exportDataRequest appContext = + let + data = + { subject = "Export my Unison Share data" + , body = "Automated support request to export my Unison Share data" + , tags = [ "export-data" ] + } + in + ShareApi.createSupportTicket data + |> HttpApi.toRequestWithEmptyResponse ExportDataRequestFinished + |> HttpApi.perform appContext.api + + +deleteAccountRequest : AppContext -> Cmd Msg +deleteAccountRequest appContext = + let + data = + { subject = "Delete my Unison Share account" + , body = "Automated support request to delete my Unison Share account" + , tags = [ "delete-account" ] + } + in + ShareApi.createSupportTicket data + |> HttpApi.toRequestWithEmptyResponse ExportDataRequestFinished + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewStatus : WebData () -> Html Msg +viewStatus status = + let + loading = + div [ class "status" ] + [ StatusBanner.working "Creating a support ticket for your request..." + ] + in + case status of + NotAsked -> + loading + + Loading -> + loading + + Success _ -> + div [ class "status" ] + [ StatusBanner.good "We've created a support ticket for your request. We'll send an update as soon as possible." + , Button.iconThenLabel CloseModal Icon.thumbsUp "Got it!" |> Button.medium |> Button.emphasized |> Button.view + ] + + Failure _ -> + div [ class "status" ] + [ StatusBanner.bad "Something didn't quite work in trying to create your request. Please try again. We've also been notified about this issue." + , Button.iconThenLabel CloseModal Icon.thumbsUp "Close" |> Button.medium |> Button.emphasized |> Button.view + ] + + +viewExportDataModal : Confirm -> Html Msg +viewExportDataModal confirm = + let + viewContent action = + Modal.Content + (section [ class "info-modal-content" ] + [ p [] [ text "We don't yet have an automated export system, and are handling requests via our support system." ] + , action + ] + ) + + content = + case confirm of + NotConfirmed -> + viewContent (Button.button ExportDataConfirm "Confirm Data Export" |> Button.emphasized |> Button.view) + + Confirmed status -> + viewContent (viewStatus status) + in + Modal.modal "info-modal" CloseModal content + |> Modal.withHeader "Export Data" + |> Modal.view + + +viewDeleteAccountModal : Account a -> Confirm -> Html Msg +viewDeleteAccountModal a confirm = + let + viewContent action = + Modal.Content + (section [ class "info-modal-content" ] + [ p [] + [ text "We're " + , em [] [ text "really" ] + , text " sorry to hear you're looking to delete your " + , strong [] [ text (UserHandle.toString a.handle) ] + , text " account." + ] + , p [] [ text "We don't yet have an automatic deletion system in place, and are handling it via our support system." ] + , action + ] + ) + + content = + case confirm of + NotConfirmed -> + viewContent (Button.button DeleteAccountConfirm "Confirm Account Deletion" |> Button.emphasized |> Button.view) + + Confirmed status -> + viewContent (viewStatus status) + in + Modal.modal "info-modal" CloseModal content + |> Modal.withHeader "Delete Account" + |> Modal.view + + +view : Account a -> Model -> AppDocument Msg +view account model = + let + content = + [ Card.titled "Account" + [ Button.iconThenLabel_ (Click.onClick ShowExportDataModal) Icon.download "Export data" |> Button.view + , div [] [ text "Download a zip of all your data on Unison Share. Including your codebase and user settings." ] + , UI.divider + , Button.iconThenLabel_ (Click.onClick ShowDeleteAccountModal) Icon.warn "Delete Account" |> Button.critical |> Button.view + , p [] [ text "Deleting your account will remove all data associated with your account. References to your authors will remain in place, but no longer tied to an account." ] + , p [] [ text "Deletion of your account can’t be completed if you own any published projects—ownership must be transfered." ] + ] + |> Card.asContained + |> Card.view + ] + + page = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn content + |> PageContent.withPageTitle + (PageTitle.title "Account Settings" + |> PageTitle.withIcon Icon.cog + |> PageTitle.withDescription "Manage your account settings" + ) + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + modal = + case model of + ExportDataModal confirm -> + Just (viewExportDataModal confirm) + + DeleteAccountModal confirm -> + Just (viewDeleteAccountModal account confirm) + + _ -> + Nothing + in + { pageId = "account" + , title = "Account" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Nothing + , page = PageLayout.view page + , modal = modal + } diff --git a/src/UnisonShare/Page/AppErrorPage.elm b/src/UnisonShare/Page/AppErrorPage.elm new file mode 100644 index 00000000..1d096aee --- /dev/null +++ b/src/UnisonShare/Page/AppErrorPage.elm @@ -0,0 +1,69 @@ +module UnisonShare.Page.AppErrorPage exposing (..) + +import Html exposing (br, p, text) +import Html.Attributes exposing (class) +import UI.Button as Button +import UI.Card as Card +import UI.Icon as Icon +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.StatusMessage as StatusMessage +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppError exposing (AppError(..)) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter + + +view : AppContext -> AppError -> AppDocument msg +view appContext appError = + let + card = + case appError of + AccountCreationGitHubPermissionsRejected -> + StatusMessage.bad "Couldn't Create Account" + [ p [] + [ text "It looks like you rejected the permission request when creating your account with GitHub." + , br [] [] + , text "We need these core pieces of data from your GitHub in order to create your Unison Share account." + ] + , p [] [ text "Please try again." ] + ] + |> StatusMessage.withCta (Button.iconThenLabel_ (Link.login appContext.api appContext.currentUrl) Icon.github "Create Account with GitHub" |> Button.medium) + |> StatusMessage.asCard + + AccountCreationHandleAlreadyTaken -> + StatusMessage.bad "Couldn't Create Account" + [ p [] + [ text "It looks like the selected handle already exists within Unison Share." + ] + , p [] [ text "Please try again with a different handle." ] + ] + |> StatusMessage.withCta (Button.iconThenLabel_ (Link.login appContext.api appContext.currentUrl) Icon.github "Create Account with GitHub" |> Button.medium) + |> StatusMessage.asCard + + UnspecifiedError -> + StatusMessage.bad "Something went wrong 😞" + [ p [] [ text "Unfortunately, we couldn't successfully complete your request." ] + , p [ class "subtle" ] + [ text "The Unison team have been notified about this error," + , br [] [] + , text " so that we can help prevent it in the future." + ] + ] + |> StatusMessage.withCta (Button.iconThenLabel_ Link.reportBug Icon.bug "Report a Bug" |> Button.medium) + |> StatusMessage.asCard + + page = + PageLayout.centeredLayout + (PageContent.oneColumn [ Card.view card ]) + PageFooter.pageFooter + in + { pageId = "error-page" + , title = "Something went wrong 😞" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Nothing + , page = PageLayout.view page + , modal = Nothing + } diff --git a/src/UnisonShare/Page/CatalogPage.elm b/src/UnisonShare/Page/CatalogPage.elm new file mode 100644 index 00000000..e2c6c8b8 --- /dev/null +++ b/src/UnisonShare/Page/CatalogPage.elm @@ -0,0 +1,586 @@ +module UnisonShare.Page.CatalogPage exposing (..) + +import Html exposing (Html, div, footer, h1, input, p, strong, table, tbody, td, text, tr) +import Html.Attributes exposing (autofocus, class, classList, placeholder) +import Html.Events exposing (onBlur, onFocus, onInput, onMouseDown) +import Json.Decode as Decode exposing (nullable, string) +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (required) +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Search as Search exposing (Search) +import Lib.SearchResults as SearchResults exposing (SearchResults(..)) +import Lib.Util exposing (decodeTag) +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.Card as Card +import UI.Icon as Icon +import UI.KeyboardShortcut as KeyboardShortcut exposing (KeyboardShortcut(..)) +import UI.KeyboardShortcut.Key as Key exposing (Key(..)) +import UI.KeyboardShortcut.KeyboardEvent as KeyboardEvent exposing (KeyboardEvent) +import UI.Modal as Modal +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.Placeholder as Placeholder +import UI.ProfileSnippet as ProfileSnippet +import UI.StatusMessage as StatusMessage +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.Catalog as Catalog exposing (Catalog, CatalogWithFeatured) +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project as Project exposing (Project, ProjectSummary) +import UnisonShare.Project.ProjectListing as ProjectListing +import UnisonShare.Project.ProjectRef as ProjectRef +import UnisonShare.Route as Route +import UnisonShare.User as User exposing (UserSummary) + + + +-- MODEL + + +{-| TODO: This should maybe include more fields and be more like ProjectSummary +-} +type alias ProjectSearchMatch = + Project + { summary : Maybe String + } + + +type Match + = UserMatch UserSummary + | ProjectMatch ProjectSearchMatch + + +type alias CatalogSearch = + Search Match + + +type PageModal + = NoModal + | GetOnTheCatalogModal + + +type alias Model = + { search : CatalogSearch + , hasFocus : Bool + , catalog : WebData Catalog + , keyboardShortcut : KeyboardShortcut.Model + , modal : PageModal + } + + +init : AppContext -> ( Model, Cmd Msg ) +init appContext = + let + model = + { search = Search.empty + , hasFocus = True + , catalog = Loading + , keyboardShortcut = KeyboardShortcut.init appContext.operatingSystem + , modal = NoModal + } + in + ( model, fetchCatalog appContext ) + + + +-- UPDATE + + +type Msg + = UpdateQuery String + | PerformSearch String + | FetchCatalogFinished (WebData Catalog) + | RetryFetchCatalog + | UpdateFocus Bool + | ClearQuery + | SearchFinished String (HttpResult (List Match)) + | SelectMatch Match + | ShowGetOnTheCatalogModal + | CloseModal + | Keydown KeyboardEvent + | KeyboardShortcutMsg KeyboardShortcut.Msg + + +update : AppContext -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext msg model = + case msg of + FetchCatalogFinished c -> + ( { model | catalog = c }, Cmd.none ) + + RetryFetchCatalog -> + ( { model | catalog = Loading }, fetchCatalog appContext ) + + UpdateFocus hasFocus -> + ( { model | hasFocus = hasFocus }, Cmd.none ) + + UpdateQuery query -> + let + search = + Search.withQuery query model.search + in + if Search.hasSubstantialQuery search then + ( { model | search = search }, Search.debounce (PerformSearch query) ) + + else + ( { model | search = search }, Cmd.none ) + + PerformSearch query -> + if Search.queryEquals query model.search then + ( { model | search = Search.toSearching model.search }, searchUsers appContext query ) + + else + ( model, Cmd.none ) + + ClearQuery -> + ( { model | search = Search.reset model.search }, Cmd.none ) + + SearchFinished query results -> + if Search.queryEquals query model.search then + ( { model | search = Search.fromResult model.search results }, Cmd.none ) + + else + ( model, Cmd.none ) + + SelectMatch match -> + let + cmd = + matchToNavigate appContext match + in + ( model, cmd ) + + ShowGetOnTheCatalogModal -> + ( { model | modal = GetOnTheCatalogModal }, Cmd.none ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none ) + + Keydown event -> + let + ( keyboardShortcut, kCmd ) = + KeyboardShortcut.collect model.keyboardShortcut event.key + + cmd = + Cmd.map KeyboardShortcutMsg kCmd + + newModel = + { model | keyboardShortcut = keyboardShortcut } + + shortcut = + KeyboardShortcut.fromKeyboardEvent model.keyboardShortcut event + in + case shortcut of + Sequence _ Escape -> + ( { newModel | search = Search.reset model.search }, cmd ) + + Sequence _ ArrowUp -> + ( { newModel | search = Search.searchResultsPrev model.search }, cmd ) + + Sequence _ ArrowDown -> + ( { newModel | search = Search.searchResultsNext model.search }, cmd ) + + Sequence _ Enter -> + case model.search of + Search.Success _ results -> + let + navigate = + results + |> SearchResults.focus + |> Maybe.map (matchToNavigate appContext) + |> Maybe.withDefault Cmd.none + in + ( newModel, Cmd.batch [ cmd, navigate ] ) + + _ -> + ( newModel, cmd ) + + Sequence (Just Semicolon) k -> + case Key.toNumber k of + Just n -> + let + navigate = + Search.searchResults model.search + |> Maybe.andThen (SearchResults.getAt (n - 1)) + |> Maybe.map (matchToNavigate appContext) + |> Maybe.withDefault Cmd.none + in + ( newModel, Cmd.batch [ cmd, navigate ] ) + + Nothing -> + ( newModel, cmd ) + + _ -> + ( newModel, cmd ) + + KeyboardShortcutMsg kMsg -> + let + ( keyboardShortcut, cmd ) = + KeyboardShortcut.update kMsg model.keyboardShortcut + in + ( { model | keyboardShortcut = keyboardShortcut }, Cmd.map KeyboardShortcutMsg cmd ) + + + +-- EFFECTS + + +searchUsers : AppContext -> String -> Cmd Msg +searchUsers appContext query = + let + makeProjectSearchMatch ref visibility summary = + { ref = ref, visibility = visibility, summary = summary } + + decodeProjectSearchMatch = + Decode.succeed makeProjectSearchMatch + |> required "projectRef" ProjectRef.decode + |> required "visibility" Project.decodeVisibility + |> required "summary" (nullable string) + + decodeMatch = + Decode.oneOf + [ when decodeTag ((==) "User") (Decode.map UserMatch User.decodeSummary) + , when decodeTag ((==) "Project") (Decode.map ProjectMatch decodeProjectSearchMatch) + ] + in + ShareApi.search query + |> HttpApi.toRequest (Decode.list decodeMatch) (SearchFinished query) + |> HttpApi.perform appContext.api + + +matchToNavigate : AppContext -> Match -> Cmd Msg +matchToNavigate appContext match = + case match of + UserMatch u -> + u.handle |> Route.userProfile |> Route.navigate appContext.navKey + + ProjectMatch p -> + p.ref |> Route.projectOverview |> Route.navigate appContext.navKey + + +fetchCatalog : AppContext -> Cmd Msg +fetchCatalog appContext = + ShareApi.catalog + |> HttpApi.toRequest Catalog.decode + (RemoteData.fromResult >> FetchCatalogFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewCatalogProject : ProjectSummary -> Html msg +viewCatalogProject project = + let + listing = + project + |> ProjectListing.projectListing + |> ProjectListing.large + |> ProjectListing.withClick Link.userProfile Link.projectOverview + |> ProjectListing.view + + summary = + case project.summary of + Just s -> + div [ class "catalog-project_summary" ] [ text s ] + + Nothing -> + UI.nothing + in + div [ class "catalog_project" ] + [ listing + , summary + ] + + +viewCategory : ( String, List ProjectSummary ) -> Html msg +viewCategory ( category, projects ) = + let + projectLinks = + projects + |> List.map viewCatalogProject + in + Card.titled category [ div [ class "catalog_projects" ] projectLinks ] + |> Card.view + + +{-| View a match in the dropdown list. Use `onMouseDown` instead of `onClick` +to avoid competing with `onBlur` on the input +-} +viewMatch : KeyboardShortcut.Model -> Match -> Bool -> Maybe Key -> Html Msg +viewMatch keyboardShortcut match isFocused shortcut = + let + shortcutIndicator = + if isFocused then + KeyboardShortcut.view keyboardShortcut (Sequence Nothing Key.Enter) + + else + case shortcut of + Nothing -> + UI.nothing + + Just key -> + KeyboardShortcut.view keyboardShortcut (Sequence (Just Key.Semicolon) key) + in + case match of + UserMatch user -> + tr + [ classList [ ( "search-result", True ), ( "focused", isFocused ) ] + , onMouseDown (SelectMatch match) + ] + [ td [ class "match-name" ] + [ div [ class "user-match" ] + [ ProfileSnippet.profileSnippet user |> ProfileSnippet.view + ] + ] + , td [ class "category" ] [ text "User" ] + , td [] [ div [ class "shortcut" ] [ shortcutIndicator ] ] + ] + + ProjectMatch project -> + let + summary = + UI.viewMaybe + (\s -> + div [ class "project-match_summary" ] + [ text s ] + ) + project.summary + in + tr + [ classList [ ( "search-result", True ), ( "focused", isFocused ) ] + , onMouseDown (SelectMatch match) + ] + [ td [ class "match-name" ] + [ div [ class "project-match" ] + [ ProjectListing.projectListing project |> ProjectListing.view + , summary + ] + ] + , td [ class "category" ] [ text "Project" ] + , td [] [ div [ class "shortcut" ] [ shortcutIndicator ] ] + ] + + +indexToShortcut : Int -> Maybe Key +indexToShortcut index = + let + n = + index + 1 + in + if n > 9 then + Nothing + + else + n |> String.fromInt |> Key.fromString |> Just + + +viewMatches : KeyboardShortcut.Model -> SearchResults.Matches Match -> Html Msg +viewMatches keyboardShortcut matches = + let + matchItems = + matches + |> SearchResults.mapMatchesToList (\d f -> ( d, f )) + |> List.indexedMap (\i ( d, f ) -> ( d, f, indexToShortcut i )) + |> List.map (\( d, f, s ) -> viewMatch keyboardShortcut d f s) + in + table [] [ tbody [] matchItems ] + + +viewSearchResults : KeyboardShortcut.Model -> CatalogSearch -> Html Msg +viewSearchResults keyboardShortcut search = + case search of + Search.Success query r -> + let + resultsPane = + case r of + SearchResults.Empty -> + div [ class "empty-state" ] [ text ("No matches found for \"" ++ query ++ "\"") ] + + SearchResults.SearchResults matches -> + viewMatches keyboardShortcut matches + in + div [ class "search-results" ] [ resultsPane ] + + _ -> + UI.nothing + + +viewGetOnTheCatalogModal : Html Msg +viewGetOnTheCatalogModal = + let + content = + Modal.Content + (div + [] + [ p [] [ text "To get listed on the Catalog make sure your project is public and have a release." ] + , p [] [ text "Make sure dependencies of your project are within a namespace called 'lib'. For instance, the 'base' dependency would be in 'lib.base.'" ] + , p [] + [ text "Then post a message in the" + , Link.view "#libraries channel on the Unison Discord" Link.discord + , text "tagging Simon (" + , strong [] [ text "@hojberg" ] + , text ") with a link to your project." + ] + , footer [ class "modal-actions" ] + [ Button.iconThenLabel CloseModal Icon.thumbsUp "Got it!" |> Button.emphasized |> Button.view + ] + ] + ) + in + Modal.modal "get-on-the-catalog-modal" CloseModal content + |> Modal.withHeader "Get listed on the Catalog" + |> Modal.view + + +viewLoadingCatalog : Html msg +viewLoadingCatalog = + let + placeholderShape length = + Placeholder.text + |> Placeholder.subdued + |> Placeholder.withLength length + |> Placeholder.view + + viewLoadingCard = + Card.card + [ placeholderShape Placeholder.Medium + , placeholderShape Placeholder.Small + , placeholderShape Placeholder.Large + , placeholderShape Placeholder.Small + , placeholderShape Placeholder.Medium + ] + |> Card.view + in + div [ class "catalog" ] + [ div [ class "categories" ] + [ viewLoadingCard + , viewLoadingCard + , viewLoadingCard + , viewLoadingCard + , viewLoadingCard + , viewLoadingCard + ] + ] + + +viewCategories : CatalogWithFeatured -> List (Html msg) +viewCategories categories = + (categories.featured + |> Maybe.map (\ps -> viewCategory ( "Featured", ps )) + |> Maybe.map List.singleton + |> Maybe.withDefault [] + ) + ++ List.map viewCategory categories.rest + + +view_ : Model -> PageLayout Msg +view_ model = + let + catalog = + case model.catalog of + NotAsked -> + viewLoadingCatalog + + Loading -> + viewLoadingCatalog + + Success catalog_ -> + let + categories = + catalog_ + |> Catalog.asFeatured + |> viewCategories + in + div [ class "catalog" ] + [ div [ class "categories" ] categories + , div [ class "get-listed-cta" ] [ Button.iconThenLabel ShowGetOnTheCatalogModal Icon.window "Get listed on the Catalog" |> Button.view ] + ] + + Failure _ -> + div [ class "catalog catalog_error" ] + [ StatusMessage.bad "Couldn't load Catalog" + [ p [] [ text "Something broke on our end and the Catalog could not be loaded" ] + ] + |> StatusMessage.withCta (Button.iconThenLabel RetryFetchCatalog Icon.refresh "Try again" |> Button.medium) + |> StatusMessage.view + ] + + searchResults = + if model.hasFocus then + viewSearchResults model.keyboardShortcut model.search + + else + UI.nothing + + keyboardEvent = + KeyboardEvent.on KeyboardEvent.Keydown Keydown + |> KeyboardEvent.stopPropagation + |> KeyboardEvent.preventDefaultWhen + (\evt -> List.member evt.key [ ArrowUp, ArrowDown, Semicolon ]) + |> KeyboardEvent.attach + in + PageLayout.heroLayout + (PageLayout.PageHero + (div [ class "catalog-hero" ] + [ h1 [] + [ div [] + [ strong [ class "explore" ] [ text "Explore" ] + , text ", " + , strong [ class "discover" ] [ text "Discover" ] + , text ", and " + , strong [ class "share" ] [ text "Share" ] + , text " Unison Code" + ] + , div [] [ text "Projects, libraries, documention, terms, and types" ] + ] + , Html.node "search" + [ class "catalog-search", keyboardEvent ] + [ div [ class "search-field" ] + [ Icon.view Icon.search + , input + [ placeholder "Search for projects and users" + , onInput UpdateQuery + , autofocus True + , onBlur (UpdateFocus False) + , onFocus (UpdateFocus True) + ] + [] + ] + , searchResults + ] + ] + ) + ) + (PageContent.oneColumn + [ catalog + ] + ) + PageFooter.pageFooter + + +view : Model -> AppDocument Msg +view model = + let + page = + model |> view_ |> PageLayout.view + + modal = + case model.modal of + NoModal -> + Nothing + + GetOnTheCatalogModal -> + Just viewGetOnTheCatalogModal + in + { pageId = "catalog-page" + , title = "Catalog" + , pageHeader = Nothing + , page = page + , appHeader = AppHeader.appHeader AppHeader.Catalog + , modal = modal + } diff --git a/src/UnisonShare/Page/CloudPage.elm b/src/UnisonShare/Page/CloudPage.elm new file mode 100644 index 00000000..9136194b --- /dev/null +++ b/src/UnisonShare/Page/CloudPage.elm @@ -0,0 +1,48 @@ +module UnisonShare.Page.CloudPage exposing (..) + +import Html exposing (h1, p, text) +import UI.Card as Card +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Session as Session exposing (Session) +import UnisonShare.SupportChatWidget as SupportChatWidget + + +view : Session -> AppDocument msg +view session = + let + content = + case session of + Session.Anonymous -> + PageContent.oneColumn + [ text "Visit " + , Link.view "unison.cloud" Link.unisonCloudWebsite + , text " for more details." + ] + + Session.SignedIn a -> + PageContent.oneColumn + [ Card.card + [ h1 [] [ text "Unison Cloud" ] + , p [] [ text "Unison Cloud is currently in early beta." ] + , p [] [ text "Reach out to us with the chat widget below." ] + ] + |> Card.view + , Link.view "Learn more about Unison Cloud." Link.unisonCloudWebsite + , SupportChatWidget.view a + ] + + page = + PageLayout.centeredLayout content PageFooter.pageFooter + in + { pageId = "cloud-page" + , title = "Unison Cloud" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Nothing + , page = PageLayout.view page + , modal = Nothing + } diff --git a/src/UnisonShare/Page/CodePage.elm b/src/UnisonShare/Page/CodePage.elm new file mode 100644 index 00000000..9040c3ae --- /dev/null +++ b/src/UnisonShare/Page/CodePage.elm @@ -0,0 +1,595 @@ +module UnisonShare.Page.CodePage exposing (..) + +import Code.CodebaseTree as CodebaseTree +import Code.Config exposing (Config) +import Code.Definition.Reference exposing (Reference) +import Code.Finder as Finder +import Code.Finder.SearchOptions as SearchOptions +import Code.FullyQualifiedName as FQN exposing (FQN) +import Code.Namespace exposing (NamespaceDetails) +import Code.Perspective as Perspective exposing (Perspective) +import Code.ReadmeCard as ReadmeCard +import Code.Workspace as Workspace +import Html exposing (Html) +import Lib.HttpApi as HttpApi +import RemoteData exposing (WebData) +import UI.KeyboardShortcut as KeyboardShortcut +import UI.KeyboardShortcut.Key exposing (Key(..)) +import UI.KeyboardShortcut.KeyboardEvent as KeyboardEvent exposing (KeyboardEvent) +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.Sidebar as Sidebar exposing (Sidebar) +import UI.ViewMode as ViewMode exposing (ViewMode) +import UnisonShare.AppContext as AppContext exposing (AppContext) +import UnisonShare.CodeBrowsingContext exposing (CodeBrowsingContext(..)) +import UnisonShare.CodebaseStatus as CodebaseStatus exposing (CodebaseStatus) +import UnisonShare.Page.CodePageContent as CodePageContent +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Route as Route exposing (CodeRoute(..)) + + + +-- MODEL + + +type PageModal + = NoModal + | FinderModal Finder.Model + | DownloadModal FQN + + +type CodeContent + = PerspectivePage ReadmeCard.Model + | WorkspacePage Workspace.Model + + +type alias Model = + { content : CodeContent + , sidebarToggled : Bool + , codebaseTree : CodebaseTree.Model + , config : Config + , modal : PageModal + , keyboardShortcut : KeyboardShortcut.Model + } + + +init : AppContext -> CodeBrowsingContext -> CodeRoute -> ( Model, Cmd Msg ) +init appContext context codeRoute = + let + config = + AppContext.toCodeConfig appContext context perspective + + perspective = + case codeRoute of + CodeRoot p -> + Perspective.fromParams p + + Definition p _ -> + Perspective.fromParams p + + ( codebaseTree, codebaseTreeCmd ) = + CodebaseTree.init config + + ( content, cmd ) = + case codeRoute of + CodeRoot _ -> + ( PerspectivePage ReadmeCard.init + , Cmd.none + ) + + Definition _ ref -> + let + ( workspace, workspaceCmd ) = + Workspace.init config (Just ref) + in + ( WorkspacePage workspace + , Cmd.map WorkspaceMsg workspaceCmd + ) + + fetchNamespaceDetailsCmd = + config.perspective + |> CodePageContent.fetchNamespaceDetails + FetchPerspectiveNamespaceDetailsFinished + context + |> Maybe.map (HttpApi.perform appContext.api) + |> Maybe.withDefault Cmd.none + in + ( { content = content + , sidebarToggled = False + , codebaseTree = codebaseTree + , config = config + , modal = NoModal + , keyboardShortcut = KeyboardShortcut.init appContext.operatingSystem + } + , Cmd.batch + [ cmd + , Cmd.map CodebaseTreeMsg codebaseTreeCmd + , fetchNamespaceDetailsCmd + ] + ) + + + +-- UPDATE + + +type Msg + = ShowFinderModal + | ShowDownloadModal FQN + | CloseModal + | UpOneLevel + | ChangePerspectiveToNamespace FQN + | ToggleSidebar + | FetchPerspectiveNamespaceDetailsFinished FQN (WebData NamespaceDetails) + | ReadmeCardMsg ReadmeCard.Msg + | Keydown KeyboardEvent + | FinderMsg Finder.Msg + | KeyboardShortcutMsg KeyboardShortcut.Msg + | CodebaseTreeMsg CodebaseTree.Msg + | WorkspaceMsg Workspace.Msg + + +update : AppContext -> CodeBrowsingContext -> ViewMode -> CodeRoute -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext context viewMode codeRoute msg model_ = + let + -- Always update the subPage since url/route changes often happens out + -- of band. + -- TODO: When is this ever needed outside of Route changes from App?, + -- like when do we need it via `update`? its supposed to be out of band + -- right? + ( model, cmd ) = + updateSubPage appContext context codeRoute model_ + in + case ( model.content, msg ) of + ( _, ShowFinderModal ) -> + let + ( fm, fCmd ) = + Finder.init model.config (SearchOptions.init model.config.perspective Nothing) + in + ( { model | modal = FinderModal fm }, Cmd.batch [ cmd, Cmd.map FinderMsg fCmd ] ) + + ( _, ShowDownloadModal fqn ) -> + ( { model | modal = DownloadModal fqn }, cmd ) + + ( _, CloseModal ) -> + ( { model | modal = NoModal }, cmd ) + + ( _, FetchPerspectiveNamespaceDetailsFinished fqn details ) -> + let + config = + model.config + + perspective = + case model.config.perspective of + Perspective.Root r -> + if FQN.isRoot fqn then + Perspective.Root { r | details = details } + + else + config.perspective + + Perspective.Namespace p -> + if FQN.equals p.fqn fqn then + Perspective.Namespace { p | details = details } + + else + config.perspective + + nextConfig = + { config | perspective = perspective } + in + ( { model | config = nextConfig }, cmd ) + + ( _, UpOneLevel ) -> + let + newPerspective = + Perspective.upOneLevel model.config.perspective + + navCmd = + navigateToCode appContext context (Route.replacePerspective (routeReference codeRoute) newPerspective) + in + ( model, Cmd.batch [ cmd, navCmd ] ) + + ( _, ChangePerspectiveToNamespace fqn ) -> + let + perspective = + Perspective.toNamespacePerspective model.config.perspective fqn + + navCmd = + navigateToCode appContext context (Route.replacePerspective (routeReference codeRoute) perspective) + in + ( model, Cmd.batch [ cmd, navCmd ] ) + + ( _, ToggleSidebar ) -> + ( { model | sidebarToggled = not model.sidebarToggled }, cmd ) + + ( _, CodebaseTreeMsg codebaseTreeMsg ) -> + let + ( codebaseTree, codebaseTreeCmd, outMsg ) = + CodebaseTree.update model.config codebaseTreeMsg model.codebaseTree + + ( m, cmd_ ) = + ( { model | codebaseTree = codebaseTree } + , Cmd.map CodebaseTreeMsg codebaseTreeCmd + ) + in + case outMsg of + CodebaseTree.None -> + ( m, cmd_ ) + + CodebaseTree.OpenDefinition ref -> + let + navCmd = + navigateToCode appContext context (Route.definition model.config.perspective ref) + + -- Close the sidebar when opening items on mobile + m_ = + if m.sidebarToggled then + { m | sidebarToggled = False } + + else + m + in + ( m_, Cmd.batch [ cmd, cmd_, navCmd ] ) + + CodebaseTree.ChangePerspectiveToNamespace fqn -> + let + perspective = + Perspective.toNamespacePerspective model.config.perspective fqn + + ref = + case codeRoute of + Definition _ r -> + Just r + + _ -> + Nothing + + navCmd = + navigateToCode appContext context (Route.replacePerspective ref perspective) + in + ( m, Cmd.batch [ cmd, cmd_, navCmd ] ) + + ( _, FinderMsg finderMsg ) -> + case model.modal of + FinderModal fm -> + let + ( fm_, fCmd, outMsg ) = + Finder.update model.config finderMsg fm + in + case outMsg of + Finder.Remain -> + ( { model | modal = FinderModal fm_ }, Cmd.batch [ cmd, Cmd.map FinderMsg fCmd ] ) + + Finder.Exit -> + ( { model | modal = NoModal }, Cmd.batch [ cmd, Cmd.map FinderMsg fCmd ] ) + + Finder.OpenDefinition r -> + ( { model | modal = NoModal } + , Cmd.batch + [ cmd + , Cmd.map FinderMsg fCmd + , navigateToCode appContext context (Route.definition model.config.perspective r) + ] + ) + + _ -> + ( model, cmd ) + + ( _, Keydown event ) -> + -- When handling keydown, we don't want to run updateSubPage + -- Since this event is handled by Workspace as well, and if both are + -- handling it with updateSubPage in play, a closed definition (by + -- hitting 'x' on the keyboard) is re-opened as the next event + -- happens before the route change to the newly focused item and thus + -- the old item is re-opened. + keydown appContext model_ event + + ( _, KeyboardShortcutMsg kMsg ) -> + let + ( keyboardShortcut, kCmd ) = + KeyboardShortcut.update kMsg model.keyboardShortcut + in + ( { model | keyboardShortcut = keyboardShortcut }, Cmd.batch [ cmd, Cmd.map KeyboardShortcutMsg kCmd ] ) + + ( PerspectivePage rm, ReadmeCardMsg readmeCardMsg ) -> + let + ( readmeCard, rmCmd, out ) = + ReadmeCard.update model.config readmeCardMsg rm + + navCmd = + case out of + ReadmeCard.OpenDefinition r -> + navigateToCode appContext context (Route.definition model.config.perspective r) + + _ -> + Cmd.none + in + ( { model | content = PerspectivePage readmeCard } + , Cmd.batch [ cmd, navCmd, Cmd.map ReadmeCardMsg rmCmd ] + ) + + ( WorkspacePage workspace, WorkspaceMsg workspaceMsg ) -> + let + ( workspace_, workspaceCmd, outMsg ) = + Workspace.update model.config viewMode workspaceMsg workspace + + ( m, outCmd ) = + case outMsg of + Workspace.Focused ref -> + ( model, navigateToCode appContext context (Route.definition model.config.perspective ref) ) + + Workspace.Emptied -> + ( model, navigateToCode appContext context (Route.codeRoot model.config.perspective) ) + + Workspace.ChangePerspectiveToSubNamespace ref subFqn -> + let + perspective = + let + fullFqn = + case model.config.perspective of + Perspective.Namespace { fqn } -> + FQN.append fqn subFqn + + _ -> + subFqn + in + Perspective.toNamespacePerspective model.config.perspective fullFqn + in + ( model, navigateToCode appContext context (Route.replacePerspective ref perspective) ) + + Workspace.ShowFinderRequest adhocFqn -> + let + ( fm, fCmd ) = + Finder.init model.config (SearchOptions.init model.config.perspective (Just adhocFqn)) + in + ( { model | modal = FinderModal fm }, Cmd.map FinderMsg fCmd ) + + _ -> + ( model, Cmd.none ) + in + ( { m | content = WorkspacePage workspace_ } + , Cmd.batch [ cmd, outCmd, Cmd.map WorkspaceMsg workspaceCmd ] + ) + + _ -> + ( model, cmd ) + + +updateSubPage : AppContext -> CodeBrowsingContext -> CodeRoute -> Model -> ( Model, Cmd Msg ) +updateSubPage appContext codeBrowsingContext codeRoute model = + let + toConfig = + AppContext.toCodeConfig appContext codeBrowsingContext + + refreshSidebar newConfig m = + CodePageContent.fetchPerspectiveAndCodebaseTree + appContext + newConfig + FetchPerspectiveNamespaceDetailsFinished + CodebaseTreeMsg + codeBrowsingContext + model.config.perspective + m + + -- Don't replace the perspective (it has namespace details state) when the page refreshes. + config p = + if Perspective.equals p model.config.perspective then + model.config + + else + toConfig p + in + case codeRoute of + CodeRoot p -> + let + persp = + p |> Perspective.fromParams + + config_ = + config persp + + ( model2, cmd ) = + refreshSidebar config_ model + + content = + case model.content of + PerspectivePage rc -> + PerspectivePage rc + + _ -> + PerspectivePage ReadmeCard.init + in + ( { model2 | config = config_, content = content }, cmd ) + + Definition p ref -> + let + persp = + p |> Perspective.fromParams + + config_ = + config persp + + ( workspace, workspaceCmd ) = + case model.content of + WorkspacePage ws -> + Workspace.open config_ ws ref + + _ -> + Workspace.init config_ (Just ref) + + model2 = + { model | config = config_, content = WorkspacePage workspace } + + ( model3, cmd ) = + refreshSidebar config_ model2 + in + ( model3 + , Cmd.batch [ Cmd.map WorkspaceMsg workspaceCmd, cmd ] + ) + + +routeReference : CodeRoute -> Maybe Reference +routeReference route = + case route of + Definition _ r -> + Just r + + _ -> + Nothing + + +keydown : AppContext -> Model -> KeyboardEvent -> ( Model, Cmd Msg ) +keydown appContext model keyboardEvent = + let + shortcut = + KeyboardShortcut.fromKeyboardEvent model.keyboardShortcut keyboardEvent + + noOp = + ( model, Cmd.none ) + + toggleSidebar = + ( { model | sidebarToggled = not model.sidebarToggled }, Cmd.none ) + in + case shortcut of + KeyboardShortcut.Chord Ctrl (B _) -> + toggleSidebar + + KeyboardShortcut.Chord Meta (B _) -> + toggleSidebar + + KeyboardShortcut.Sequence _ Escape -> + ( { model | modal = NoModal }, Cmd.none ) + + _ -> + if Finder.isShowFinderKeyboardShortcut appContext.operatingSystem shortcut then + let + ( finder, cmd ) = + Finder.init model.config + (SearchOptions.init model.config.perspective Nothing) + in + ( { model | modal = FinderModal finder }, Cmd.map FinderMsg cmd ) + + else + noOp + + + +-- EFFECTS + + +navigateToCode : AppContext -> CodeBrowsingContext -> CodeRoute -> Cmd Msg +navigateToCode appContext context codeRoute = + let + route_ = + case context of + UserCode h -> + Route.userCode h codeRoute + + ProjectBranch ps bs -> + Route.projectBranch ps bs codeRoute + in + Route.navigate appContext.navKey route_ + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions model = + case model.content of + PerspectivePage _ -> + KeyboardEvent.subscribe KeyboardEvent.Keydown Keydown + + WorkspacePage ws -> + Sub.batch + [ KeyboardEvent.subscribe KeyboardEvent.Keydown Keydown + , Sub.map WorkspaceMsg (Workspace.subscriptions ws) + ] + + + +-- VIEW + + +viewContent : ViewMode -> Perspective -> CodeContent -> PageContent Msg +viewContent viewMode perspective content = + case content of + PerspectivePage readmeCard -> + PageContent.oneColumn + (CodePageContent.viewPerspectiveLandingPage + ReadmeCardMsg + ShowFinderModal + perspective + readmeCard + ) + + WorkspacePage workspace -> + PageContent.oneColumn [ Html.map WorkspaceMsg (Workspace.view viewMode workspace) ] + + +viewSidebar : CodebaseStatus -> Model -> Sidebar Msg +viewSidebar codebaseStatus model = + let + codebaseTree = + Just { codebaseTree = model.codebaseTree, codebaseTreeMsg = CodebaseTreeMsg } + in + case codebaseStatus of + CodebaseStatus.NotEmpty -> + CodePageContent.viewSidebar + model.config.perspective + { upOneLevelMsg = UpOneLevel + , showDownloadModalMsg = ShowDownloadModal + , showFinderModalMsg = ShowFinderModal + , changePerspectiveToNamespaceMsg = ChangePerspectiveToNamespace + } + codebaseTree + |> Sidebar.withToggle + { isToggled = model.sidebarToggled, toggleMsg = ToggleSidebar } + + CodebaseStatus.Empty -> + Sidebar.empty "main-sidebar" + + +view : AppContext -> (Msg -> msg) -> ViewMode -> CodeBrowsingContext -> CodebaseStatus -> Model -> ( PageLayout msg, Maybe (Html msg) ) +view appContext toMsg viewMode context codebaseStatus model = + let + content = + PageContent.map toMsg (viewContent viewMode model.config.perspective model.content) + + modal = + case model.modal of + NoModal -> + Nothing + + FinderModal fm -> + Just (Html.map toMsg (Html.map FinderMsg (Finder.view fm))) + + DownloadModal fqn -> + Just (Html.map toMsg (CodePageContent.viewDownloadModal CloseModal context fqn)) + in + case ( model.content, viewMode ) of + ( PerspectivePage _, ViewMode.Regular ) -> + ( PageLayout.sidebarLeftContentLayout + appContext.operatingSystem + (Sidebar.map toMsg (viewSidebar codebaseStatus model)) + content + PageFooter.pageFooter + |> PageLayout.withSidebarToggle model.sidebarToggled + , modal + ) + + ( WorkspacePage _, ViewMode.Regular ) -> + ( PageLayout.sidebarLeftContentLayout + appContext.operatingSystem + (Sidebar.map toMsg (viewSidebar codebaseStatus model)) + content + PageFooter.pageFooter + |> PageLayout.withSidebarToggle model.sidebarToggled + |> PageLayout.withSubduedBackground + , modal + ) + + ( _, ViewMode.Presentation ) -> + ( PageLayout.PresentationLayout content, modal ) diff --git a/src/UnisonShare/Page/CodePageContent.elm b/src/UnisonShare/Page/CodePageContent.elm new file mode 100644 index 00000000..ef162b85 --- /dev/null +++ b/src/UnisonShare/Page/CodePageContent.elm @@ -0,0 +1,338 @@ +{- Various shared setup for pages that include codebase browsing, user page, + codebase page, project page. +-} + + +module UnisonShare.Page.CodePageContent exposing (..) + +import Code.CodebaseTree as CodebaseTree +import Code.Config exposing (Config) +import Code.Definition.Readme exposing (Readme) +import Code.EmptyState as EmptyState +import Code.FullyQualifiedName as FQN exposing (FQN) +import Code.Hash as Hash +import Code.Namespace as Namespace exposing (NamespaceDetails) +import Code.Perspective as Perspective exposing (Perspective) +import Code.ReadmeCard as ReadmeCard +import Html exposing (Html, div, h3, p, section, text) +import Html.Attributes exposing (class, classList) +import Http +import Lib.HttpApi as HttpApi +import Lib.UserHandle as UserHandle +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.Card as Card +import UI.Click as Click +import UI.CopyField as CopyField +import UI.ErrorCard as ErrorCard +import UI.Icon as Icon +import UI.Modal as Modal +import UI.Placeholder as Placeholder +import UI.Sidebar as Sidebar exposing (Sidebar) +import UI.Tooltip as Tooltip +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.CodeBrowsingContext exposing (CodeBrowsingContext(..)) +import UnisonShare.Project.ProjectRef as ProjectRef + + + +-- EFFECTS + + +fetchPerspectiveAndCodebaseTree : + AppContext + -> Config + -> (FQN -> WebData NamespaceDetails -> msg) + -> (CodebaseTree.Msg -> msg) + -> CodeBrowsingContext + -> Perspective + -> { m | codebaseTree : CodebaseTree.Model } + -> ( { m | codebaseTree : CodebaseTree.Model }, Cmd msg ) +fetchPerspectiveAndCodebaseTree appContext config finishedMsg codebaseTreeMsg context oldPerspective model = + let + ( codebaseTree, codebaseTreeCmd ) = + CodebaseTree.init config + + fetchNamespaceDetailsCmd = + config.perspective + |> fetchNamespaceDetails finishedMsg context + |> Maybe.map (HttpApi.perform appContext.api) + |> Maybe.withDefault Cmd.none + in + if not (Perspective.equals oldPerspective config.perspective) && Perspective.needsFetching config.perspective then + ( { model | codebaseTree = codebaseTree } + , Cmd.batch + [ Cmd.map codebaseTreeMsg codebaseTreeCmd + , fetchNamespaceDetailsCmd + ] + ) + + else if not (Perspective.equals oldPerspective config.perspective) then + ( { model | codebaseTree = codebaseTree }, Cmd.map codebaseTreeMsg codebaseTreeCmd ) + + else + ( model, Cmd.none ) + + +fetchNamespaceDetails : + (FQN -> WebData NamespaceDetails -> msg) + -> CodeBrowsingContext + -> Perspective + -> Maybe (HttpApi.ApiRequest NamespaceDetails msg) +fetchNamespaceDetails finishedMsg context perspective = + let + fqn_ = + case perspective of + Perspective.Namespace { fqn } -> + fqn + + _ -> + FQN.root + in + fqn_ + |> ShareApi.namespace context perspective + |> HttpApi.toRequest Namespace.decodeDetails (RemoteData.fromResult >> finishedMsg fqn_) + |> Just + + + +-- VIEW + + +viewLoading : Html msg +viewLoading = + [ Placeholder.text |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Huge |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Large |> Placeholder.view + ] + |> Card.titled "Loading..." + |> Card.view + + +viewError : Http.Error -> Html msg +viewError _ = + ErrorCard.empty |> ErrorCard.view + + +viewReadme : + (ReadmeCard.Msg -> msg) + -> ReadmeCard.Model + -> Maybe Readme + -> Html msg + -> Html msg +viewReadme readmeCardMsg readmeCard readme emptyState = + readme + |> Maybe.map (ReadmeCard.view readmeCard) + |> Maybe.map (Html.map readmeCardMsg) + |> Maybe.withDefault emptyState + + +viewDownloadModal : msg -> CodeBrowsingContext -> FQN -> Html msg +viewDownloadModal closeMsg context fqn = + let + prettyName = + FQN.toString fqn + + unqualified = + FQN.unqualifiedName fqn + + local = + -- It's likely a project, so suggest the name 1 level before "latest" + if unqualified == "latest" && FQN.numSegments fqn > 2 then + "lib." ++ FQN.unqualifiedName (FQN.dropLast fqn) + + else + "lib." ++ unqualified + + remote = + case context of + ProjectBranch ps _ -> + -- TODO: is this what we want? + -- TODO: include BranchRef + ProjectRef.toString ps ++ "." ++ prettyName + + UserCode handle -> + UserHandle.toUnprefixedString handle ++ "." ++ prettyName + + pullCommand = + "pull " ++ remote ++ " " ++ local + + content = + Modal.Content + (section + [] + [ p [] [ text "Download ", UI.bold prettyName, text " by pulling the namespace from Unison Share into a namespace in your local codebase:" ] + , CopyField.copyField (\_ -> closeMsg) pullCommand |> CopyField.withPrefix ".>" |> CopyField.view + , div [ class "hint" ] [ text "Copy and paste this command into UCM." ] + ] + ) + in + Modal.modal "download-modal" closeMsg content + |> Modal.withHeader ("Download " ++ prettyName) + |> Modal.view + + +viewNamespacePerspectiveHeader : (FQN -> msg) -> { a | fqn : FQN, details : WebData NamespaceDetails } -> Html msg +viewNamespacePerspectiveHeader changePerspectiveToNamespaceMsg { fqn } = + let + -- Imprecise, but close enough, approximation of overflowing, + -- which results in a slight faded left edge A better way would + -- be to measure the DOM like we do for overflowing docs, but + -- thats quite involved... + isOverflowing = + fqn |> FQN.toString |> String.length |> (\l -> l > 20) + + toClick fqn_ = + Click.onClick (changePerspectiveToNamespaceMsg fqn_) + in + div [ classList [ ( "namespace-header", True ), ( "is-overflowing", isOverflowing ) ] ] + [ Icon.view Icon.folderOutlined + , h3 [ class "namespace" ] [ FQN.viewClickable toClick fqn ] + ] + + +viewPerspectiveHeader : + (FQN -> msg) + -> msg + -> (FQN -> msg) + -> Perspective + -> Maybe (Sidebar.SidebarHeader msg) +viewPerspectiveHeader changePerspectiveToNamespaceMsg upOneLevelMsg showDownloadModalMsg perspective = + case perspective of + Perspective.Root _ -> + Nothing + + Perspective.Namespace ns -> + let + downloadButtonLabel = + case ns.details of + Success d -> + "Download—" ++ Hash.toShortString (Namespace.hash d) + + _ -> + "Download" + + upToDestination = + if FQN.numSegments ns.fqn == 1 then + text "Overview" + + else + FQN.dropLast ns.fqn |> FQN.view + in + [ viewNamespacePerspectiveHeader changePerspectiveToNamespaceMsg ns + , div [ class "perspective-actions" ] + [ Tooltip.tooltip + (Tooltip.rich upToDestination) + |> Tooltip.withArrow Tooltip.Start + |> Tooltip.view + (Button.icon upOneLevelMsg + Icon.arrowLeftUp + |> Button.small + |> Button.view + ) + , div [ class "download" ] + [ Button.iconThenLabel + (showDownloadModalMsg ns.fqn) + Icon.download + downloadButtonLabel + |> Button.small + |> Button.view + ] + ] + , UI.divider + ] + |> Sidebar.SidebarHeader + |> Just + + +viewSidebar : + Perspective + -> + { upOneLevelMsg : msg + , showDownloadModalMsg : FQN -> msg + , showFinderModalMsg : msg + , changePerspectiveToNamespaceMsg : FQN -> msg + } + -> Maybe { codebaseTree : CodebaseTree.Model, codebaseTreeMsg : CodebaseTree.Msg -> msg } + -> Sidebar msg +viewSidebar perspective cfg codebaseTree = + let + perspectiveHeader = + viewPerspectiveHeader + cfg.changePerspectiveToNamespaceMsg + cfg.upOneLevelMsg + cfg.showDownloadModalMsg + perspective + + codeSection = + Maybe.map + (\c -> + Sidebar.section "Code" [ Html.map c.codebaseTreeMsg (CodebaseTree.view c.codebaseTree) ] + |> Sidebar.sectionWithTitleButton (Button.iconThenLabel cfg.showFinderModalMsg Icon.browse "Search" |> Button.small) + |> Sidebar.sectionWithScrollable + ) + codebaseTree + + sidebar = + case codeSection of + Just cs -> + Sidebar.sidebar_ "main-sidebar" perspectiveHeader + |> Sidebar.withSection cs + + Nothing -> + Sidebar.sidebar_ "main-sidebar" perspectiveHeader + + withCollapsedContext s = + case perspective of + Perspective.Root _ -> + s + + Perspective.Namespace ns -> + Sidebar.withCollapsedContext + (viewNamespacePerspectiveHeader + cfg.changePerspectiveToNamespaceMsg + ns + ) + s + in + sidebar + |> withCollapsedContext + |> Sidebar.withCollapsedActions [ Button.icon cfg.showFinderModalMsg Icon.browse |> Button.small ] + + +viewPerspectiveLandingPage : + (ReadmeCard.Msg -> msg) + -> msg + -> Perspective + -> ReadmeCard.Model + -> List (Html msg) +viewPerspectiveLandingPage readmeCardMsg showFinderModalMsg perspective readmeCard = + let + ( details_, emptyState ) = + case perspective of + Perspective.Root { details } -> + ( details, EmptyState.view "Browse Code" (Click.onClick showFinderModalMsg) ) + + Perspective.Namespace { fqn, details } -> + ( details, EmptyState.view (FQN.toString fqn) (Click.onClick showFinderModalMsg) ) + in + case details_ of + NotAsked -> + [ viewLoading ] + + Loading -> + [ viewLoading ] + + Success d -> + [ viewReadme + readmeCardMsg + readmeCard + (Namespace.readme d) + emptyState + ] + + Failure _ -> + -- TODO: renable after API supports root namespace details [ viewError err ] + [ EmptyState.view "Browse Code" (Click.onClick showFinderModalMsg) ] diff --git a/src/UnisonShare/Page/ErrorPage.elm b/src/UnisonShare/Page/ErrorPage.elm new file mode 100644 index 00000000..5d0ae978 --- /dev/null +++ b/src/UnisonShare/Page/ErrorPage.elm @@ -0,0 +1,39 @@ +module UnisonShare.Page.ErrorPage exposing (..) + +import Html exposing (details, div, summary, text) +import Http +import Lib.Util as Util +import UI +import UI.Card as Card +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle +import UI.StatusBanner as StatusBanner +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Session as Session exposing (Session) + + +view : Session -> Http.Error -> String -> String -> PageLayout msg +view session error entityName className = + let + errorDetails = + if Session.isUnisonMember session then + details [] [ summary [] [ text "Error Details" ], div [] [ text (Util.httpErrorToString error) ] ] + + else + UI.nothing + in + PageLayout.centeredNarrowLayout + (PageContent.oneColumn + [ Card.card + [ StatusBanner.bad ("Something broke on our end and we couldn't show the " ++ entityName ++ ". Please try again.") + , errorDetails + ] + |> Card.withClassName className + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (PageTitle.title "Error") + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground diff --git a/src/UnisonShare/Page/NotFoundPage.elm b/src/UnisonShare/Page/NotFoundPage.elm new file mode 100644 index 00000000..5609b39f --- /dev/null +++ b/src/UnisonShare/Page/NotFoundPage.elm @@ -0,0 +1,35 @@ +module UnisonShare.Page.NotFoundPage exposing (..) + +import Html exposing (text) +import UI.Card as Card +import UI.Icon as Icon +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.PageTitle as PageTitle +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.PageFooter as PageFooter + + +view : AppDocument msg +view = + let + content = + [ Card.card [ text "Sorry, we can't find that page." ] |> Card.view + ] + + page = + PageLayout.centeredLayout + (PageContent.oneColumn content + |> PageContent.withPageTitle + (PageTitle.title "Page not found" |> PageTitle.withIcon Icon.warn) + ) + PageFooter.pageFooter + in + { pageId = "not-found" + , title = "Page not found" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Nothing + , page = PageLayout.view page + , modal = Nothing + } diff --git a/src/UnisonShare/Page/PrivacyPolicyPage.elm b/src/UnisonShare/Page/PrivacyPolicyPage.elm new file mode 100644 index 00000000..143ab2e9 --- /dev/null +++ b/src/UnisonShare/Page/PrivacyPolicyPage.elm @@ -0,0 +1,38 @@ +module UnisonShare.Page.PrivacyPolicyPage exposing (..) + +import Html exposing (div) +import Html.Attributes exposing (class) +import Markdown +import UI.Card as Card +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.PageTitle as PageTitle +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.PageFooter as PageFooter + + +view : AppDocument msg +view = + let + content = + Card.card [ div [ class "definition-doc" ] [ Markdown.toHtml [] "require:src/privacy-policy.md" ] ] + |> Card.asContained + |> Card.view + + page = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ content ] + |> PageContent.withPageTitle + (PageTitle.title "Unison Computing Privacy Policy") + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + in + { pageId = "privacy-policy" + , title = "Privacy Policy" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Nothing + , page = PageLayout.view page + , modal = Nothing + } diff --git a/src/UnisonShare/Page/ProjectBranchesPage.elm b/src/UnisonShare/Page/ProjectBranchesPage.elm new file mode 100644 index 00000000..c48c7620 --- /dev/null +++ b/src/UnisonShare/Page/ProjectBranchesPage.elm @@ -0,0 +1,366 @@ +module UnisonShare.Page.ProjectBranchesPage exposing (..) + +import Code.Branch as Branch +import Code.BranchRef as BranchRef exposing (BranchRef) +import Html exposing (Html, br, div, footer, p, span, strong, text) +import Html.Attributes exposing (class) +import Html.Keyed as Keyed +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.Card as Card +import UI.Click as Click +import UI.DateTime as DateTime +import UI.Icon as Icon +import UI.Modal as Modal +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UI.StatusIndicator as StatusIndicator +import UI.Tooltip as Tooltip +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.BranchSummary exposing (BranchSummary) +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project as Project exposing (ProjectDetails) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Session as Session exposing (Session) + + + +-- MODEL +{- TODO: Pagination, group (contributor and maintainer), search, and empty state (however you got to that) -} + + +type alias DeleteBranch = + WebData () + + +type Modal + = NoModal + | DeleteBranchModal BranchRef DeleteBranch + + +type alias Model = + { branches : WebData (List BranchSummary) + , modal : Modal + } + + +init : AppContext -> ProjectRef -> ( Model, Cmd Msg ) +init appContext projectRef = + ( { branches = Loading, modal = NoModal }, fetchBranches appContext projectRef ) + + + +-- UPDATE + + +type Msg + = FetchBranchesFinished (WebData (List BranchSummary)) + | ShowDeleteBranchModal BranchRef + | CloseModal + | YesDeleteBranch + | DeleteBranchFinished (HttpResult ()) + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef msg model = + case msg of + FetchBranchesFinished branches -> + ( { model | branches = branches }, Cmd.none ) + + ShowDeleteBranchModal branchRef -> + ( { model | modal = DeleteBranchModal branchRef NotAsked }, Cmd.none ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none ) + + YesDeleteBranch -> + case model.modal of + DeleteBranchModal branchRef _ -> + ( { model | modal = DeleteBranchModal branchRef Loading } + , deleteBranch appContext projectRef branchRef + ) + + _ -> + ( model, Cmd.none ) + + DeleteBranchFinished r -> + case model.modal of + DeleteBranchModal branchRef _ -> + let + branches = + model.branches + |> RemoteData.map + (List.filter (.ref >> BranchRef.equals branchRef >> not)) + in + ( { model + | branches = branches + , modal = DeleteBranchModal branchRef (RemoteData.fromResult r) + } + , Util.delayMsg 1500 CloseModal + ) + + _ -> + ( model, Cmd.none ) + + + +-- EFFECTS + + +fetchBranches : AppContext -> ProjectRef -> Cmd Msg +fetchBranches appContext projectRef = + let + params = + { kind = ShareApi.AllBranches Nothing + , searchQuery = Nothing + , limit = 100 + , cursor = Nothing + } + in + ShareApi.projectBranches projectRef params + |> HttpApi.toRequest + (Decode.field "items" (Decode.list (Branch.decodeSummary Project.decode))) + (RemoteData.fromResult >> FetchBranchesFinished) + |> HttpApi.perform appContext.api + + +deleteBranch : AppContext -> ProjectRef -> BranchRef -> Cmd Msg +deleteBranch appContext projectRef branchRef = + ShareApi.deleteProjectBranch projectRef branchRef + |> HttpApi.toRequestWithEmptyResponse DeleteBranchFinished + |> HttpApi.perform appContext.api + + + +-- VIEW + + +pageTitle : ProjectRef -> PageTitle.PageTitle msg +pageTitle projectRef = + PageTitle.title "Project Branches" + |> PageTitle.withDescription ("All branches for " ++ ProjectRef.toString projectRef) + + +viewLoadingPage : ProjectRef -> PageLayout msg +viewLoadingPage projectRef = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (pageTitle projectRef) + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +{-| TODO: wording here should talk about it only deleting it on Share +-} +viewDeleteBranchModal : ProjectRef -> BranchRef -> DeleteBranch -> Html Msg +viewDeleteBranchModal projectRef branchRef deleting = + let + projectRef_ = + ProjectRef.toString projectRef + + branchRef_ = + BranchRef.toString branchRef + + ( statusBanner, overlay ) = + case deleting of + NotAsked -> + ( UI.nothing, UI.nothing ) + + Loading -> + ( StatusBanner.working "Deleting..", div [ class "delete-branch-modal_overlay-deleting" ] [] ) + + Success _ -> + ( UI.nothing + , div + [ class "delete-branch-modal_overlay-success" + ] + [ StatusIndicator.good |> StatusIndicator.large |> StatusIndicator.view + , div [] + [ strong [] [ text branchRef_ ] + , br [] [] + , text " successfully deleted" + ] + ] + ) + + Failure _ -> + ( StatusBanner.bad "Delete branch failed", UI.nothing ) + + content = + div [ class "delete-branch-modal_content" ] + [ p [] + [ text "You're about to permanently delete the branch " + , strong [] [ text branchRef_ ] + , text " from " + , strong [] [ text projectRef_ ] + , text ", is that ok?" + ] + , footer + [ class "delete-branch-modal_actions" ] + [ statusBanner + , Button.button CloseModal "Cancel" + |> Button.subdued + |> Button.medium + |> Button.view + , Button.button YesDeleteBranch "Yes, delete branch" + |> Button.critical + |> Button.medium + |> Button.view + ] + , overlay + ] + in + Modal.modal "delete-branch-modal" CloseModal (Modal.Content content) + |> Modal.withHeader "Permanently Delete Branch?" + |> Modal.view + + +viewAt : AppContext -> BranchSummary -> Html msg +viewAt appContext branch = + let + at_ location = + Keyed.node "div" + [] + [ ( DateTime.toISO8601 branch.updatedAt ++ location + , span [] + [ text + (DateTime.toString + (DateTime.DistanceFrom appContext.now) + appContext.timeZone + branch.updatedAt + ) + ] + ) + ] + + tooltip = + Tooltip.rich + (div [ class "branch-updated-at_tooltip" ] + [ strong [] [ text (BranchRef.toString branch.ref) ] + , text "was last updated" + , at_ "tooltip" + ] + ) + |> Tooltip.tooltip + in + Tooltip.view + (div [ class "branch-updated-at" ] + [ Icon.view Icon.clock + , at_ "row" + ] + ) + tooltip + + +canDelete : Session -> ProjectDetails -> BranchRef -> Bool +canDelete session project branchRef = + case branchRef of + BranchRef.ContributorBranchRef h _ -> + Session.isHandle h session + + _ -> + Session.hasProjectAccess project.ref session && project.defaultBranch /= Just branchRef + + +viewBranchRow : AppContext -> ProjectDetails -> BranchSummary -> Html Msg +viewBranchRow appContext project branch = + let + del = + if canDelete appContext.session project branch.ref then + Button.icon (ShowDeleteBranchModal branch.ref) Icon.trash + |> Button.small + |> Button.subdued + |> Button.view + + else + UI.nothing + in + div [ class "project-branches_branch-row" ] + [ div [ class "project-branches_branch-info" ] + [ Click.view [] [ strong [] [ text (BranchRef.toString branch.ref) ] ] (Link.projectBranchRoot project.ref branch.ref) + , viewAt appContext branch + ] + , del + ] + + +viewPageContent : AppContext -> ProjectDetails -> List BranchSummary -> PageContent Msg +viewPageContent appContext project branches = + let + card = + branches + |> List.map (viewBranchRow appContext project) + |> div [ class "project-branches_list" ] + |> List.singleton + |> Card.card + |> Card.asContained + in + PageContent.oneColumn + [ Card.view card ] + |> PageContent.withPageTitle (pageTitle project.ref) + + +view : AppContext -> ProjectDetails -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext project model = + case model.branches of + NotAsked -> + ( viewLoadingPage project.ref, Nothing ) + + Loading -> + ( viewLoadingPage project.ref, Nothing ) + + Success branches -> + let + modal = + case model.modal of + NoModal -> + Nothing + + DeleteBranchModal branchRef del -> + if canDelete appContext.session project branchRef then + Just (viewDeleteBranchModal project.ref branchRef del) + + else + Nothing + in + ( PageLayout.centeredNarrowLayout + (viewPageContent appContext project branches) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Failure _ -> + -- TODO + ( PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ text "Couldn't load branches..." ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , Nothing + ) diff --git a/src/UnisonShare/Page/ProjectContributionChangesPage.elm b/src/UnisonShare/Page/ProjectContributionChangesPage.elm new file mode 100644 index 00000000..ed792810 --- /dev/null +++ b/src/UnisonShare/Page/ProjectContributionChangesPage.elm @@ -0,0 +1,365 @@ +module UnisonShare.Page.ProjectContributionChangesPage exposing (..) + +import Code.Definition.Reference as Reference exposing (Reference(..)) +import Code.FullyQualifiedName as FQN +import Code.Hash as Hash +import Code.Perspective as Perspective +import Html exposing (Html, div, h2, span, text) +import Html.Attributes exposing (class) +import Http +import Lib.HttpApi as HttpApi +import List.Nonempty as NEL +import RemoteData exposing (RemoteData(..), WebData) +import String.Extra exposing (pluralize) +import UI +import UI.Card as Card +import UI.Divider as Divider +import UI.Icon as Icon +import UI.PageContent as PageContent exposing (PageContent) +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UI.TabList as TabList +import UI.Tooltip as Tooltip +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Contribution exposing (Contribution) +import UnisonShare.Contribution.ContributionRef exposing (ContributionRef) +import UnisonShare.Diff as Diff exposing (Diff) +import UnisonShare.Link as Link +import UnisonShare.Project.ProjectRef exposing (ProjectRef) + + + +-- MODEL + + +type alias Model = + { diff : WebData Diff + } + + +type alias DiffBranches = + { oldBranch : Diff.DiffBranchRef + , newBranch : Diff.DiffBranchRef + } + + +init : AppContext -> ProjectRef -> ContributionRef -> ( Model, Cmd Msg ) +init appContext projectRef contribRef = + ( { diff = Loading }, fetchDiff appContext projectRef contribRef ) + + + +-- UPDATE + + +type Msg + = FetchDiffFinished (WebData Diff) + + +update : AppContext -> ProjectRef -> ContributionRef -> Msg -> Model -> ( Model, Cmd Msg ) +update _ _ _ msg model = + case msg of + FetchDiffFinished contribDiff -> + ( { model | diff = contribDiff }, Cmd.none ) + + + +-- EFFECTS + + +fetchDiff : AppContext -> ProjectRef -> ContributionRef -> Cmd Msg +fetchDiff appContext projectRef contributionRef = + ShareApi.projectContributionDiff projectRef contributionRef + |> HttpApi.toRequest Diff.decode (RemoteData.fromResult >> FetchDiffFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +branchLink : ProjectRef -> Diff.DiffBranchRef -> Reference -> Html Msg -> Html Msg +branchLink projectRef diffBranchRef ref label = + Link.projectBranchDefinition + projectRef + diffBranchRef.ref + (Perspective.absoluteRootPerspective diffBranchRef.hash) + ref + |> Link.view_ label + + +viewDiffIcon : Diff.DefinitionDiff -> Html Msg +viewDiffIcon defDiff = + let + ( className, icon, tooltipContent ) = + case defDiff of + Diff.Added _ -> + ( "added", Icon.largePlus, "Added" ) + + Diff.Removed _ -> + ( "removed", Icon.trash, "Removed" ) + + Diff.Updated _ -> + ( "updated", Icon.writingPad, "Updated" ) + + Diff.RenamedFrom _ -> + ( "renamed", Icon.tag, "Renamed" ) + + Diff.Aliased _ -> + ( "aliased", Icon.tags, "Aliased" ) + in + Tooltip.text tooltipContent + |> Tooltip.tooltip + |> Tooltip.withArrow Tooltip.Start + |> Tooltip.view (span [ class "diff-icon", class className ] [ Icon.view icon ]) + + +viewDiffLineDefinitionIcon : Diff.DiffLine -> Html msg +viewDiffLineDefinitionIcon diffLine = + let + ( description, icon ) = + case diffLine of + Diff.TermDiffLine _ -> + ( "Term", Icon.term ) + + Diff.TypeDiffLine _ -> + ( "Type", Icon.type_ ) + + Diff.DocDiffLine _ -> + ( "Doc", Icon.doc ) + + Diff.AbilityDiffLine _ -> + ( "Ability", Icon.ability ) + + Diff.AbilityConstructorDiffLine _ -> + ( "Ability Constructor", Icon.abilityConstructor ) + + Diff.DataConstructorDiffLine _ -> + ( "Data Constructor", Icon.dataConstructor ) + + Diff.TestDiffLine _ -> + ( "Test", Icon.test ) + + Diff.NamespaceDiffLine _ -> + ( "Namespace", Icon.folder ) + in + div [ class "def-icon-anchor" ] + [ Tooltip.text description + |> Tooltip.tooltip + |> Tooltip.withArrow Tooltip.Start + |> Tooltip.view (span [ class "def-icon" ] [ Icon.view icon ]) + ] + + +viewDiffLine : ProjectRef -> DiffBranches -> Diff.DiffLine -> Html Msg +viewDiffLine projectRef diffBranches diffLine = + let + sourceBranchLink_ ref label = + branchLink projectRef diffBranches.newBranch ref label + + targetBranchLink_ ref label = + branchLink projectRef diffBranches.oldBranch ref label + + viewDefinitionDiff_ refCtor prefix defDiff = + let + prefix_ = + if String.isEmpty prefix then + UI.nothing + + else + span [ class "prefix" ] [ text prefix ] + in + case defDiff of + Diff.Added { hash, shortName, fullName } -> + span + [ class "diff-info" ] + [ prefix_ + , sourceBranchLink_ (Reference.fromFQN refCtor fullName) (FQN.view shortName) + , sourceBranchLink_ (Reference.fromFQN refCtor fullName) (Hash.view hash) + ] + + Diff.Removed { hash, shortName, fullName } -> + span + [ class "diff-info" ] + [ prefix_ + , targetBranchLink_ (Reference.fromFQN refCtor fullName) (FQN.view shortName) + , targetBranchLink_ (Reference.fromFQN refCtor fullName) (Hash.view hash) + ] + + Diff.Updated { oldHash, newHash, shortName, fullName } -> + span + [ class "diff-info" ] + [ prefix_ + , sourceBranchLink_ (Reference.fromFQN refCtor fullName) (FQN.view shortName) + , sourceBranchLink_ (Reference.fromFQN refCtor fullName) (Hash.view newHash) + , span [ class "extra-info" ] + [ text " (updated from " + , targetBranchLink_ (Reference.fromFQN refCtor fullName) (Hash.view oldHash) + , text ")" + ] + ] + + Diff.RenamedFrom { hash, oldNames, newShortName, newFullName } -> + span [ class "diff-info" ] + [ prefix_ + , sourceBranchLink_ (Reference.fromFQN refCtor newFullName) (FQN.view newShortName) + , sourceBranchLink_ (Reference.fromFQN refCtor newFullName) (Hash.view hash) + , span [ class "extra-info" ] + (text "(was " + :: (oldNames + |> NEL.map + (\fqn -> targetBranchLink_ (Reference.fromFQN refCtor fqn) (FQN.view fqn)) + |> NEL.toList + |> List.intersperse (text ", ") + ) + ++ [ text ")" ] + ) + ] + + Diff.Aliased { hash, aliasShortName, aliasFullName, otherNames } -> + span [ class "diff-info" ] + [ prefix_ + , sourceBranchLink_ (Reference.fromFQN refCtor aliasFullName) (FQN.view aliasShortName) + , sourceBranchLink_ (Reference.fromFQN refCtor aliasFullName) (Hash.view hash) + , span [ class "extra-info" ] + (text "(AKA " + :: (otherNames + |> NEL.map + (\fqn -> sourceBranchLink_ (Reference.fromFQN refCtor fqn) (FQN.view fqn)) + |> NEL.toList + |> List.intersperse (text ", ") + ) + ++ [ text ")" ] + ) + ] + + defIcon = + viewDiffLineDefinitionIcon diffLine + + viewDiffLine_ diffIcon diffContent = + [ diffIcon + , defIcon + , diffContent + ] + + ( diffLineClass, content ) = + case diffLine of + Diff.TermDiffLine d -> + ( "term", viewDiffLine_ (viewDiffIcon d) (viewDefinitionDiff_ TermReference "" d) ) + + Diff.TypeDiffLine d -> + ( "type", viewDiffLine_ (viewDiffIcon d) (viewDefinitionDiff_ TypeReference "type" d) ) + + Diff.DocDiffLine d -> + ( "doc", viewDiffLine_ (viewDiffIcon d) (viewDefinitionDiff_ TermReference "" d) ) + + Diff.AbilityDiffLine d -> + ( "ability", viewDiffLine_ (viewDiffIcon d) (viewDefinitionDiff_ TypeReference "ability" d) ) + + Diff.AbilityConstructorDiffLine d -> + ( "ability-constructor", viewDiffLine_ (viewDiffIcon d) (viewDefinitionDiff_ AbilityConstructorReference "" d) ) + + Diff.DataConstructorDiffLine d -> + ( "data-constructor", viewDiffLine_ (viewDiffIcon d) (viewDefinitionDiff_ DataConstructorReference "" d) ) + + Diff.TestDiffLine d -> + ( "test", viewDiffLine_ (viewDiffIcon d) (viewDefinitionDiff_ TermReference "" d) ) + + Diff.NamespaceDiffLine ns -> + viewNamespaceLine projectRef diffBranches ns + in + div [ class "diff-line", class diffLineClass ] content + + +viewContributionDiffGroup : ProjectRef -> DiffBranches -> List Diff.DiffLine -> Html Msg +viewContributionDiffGroup projectRef diffBranches lines = + div [ class "contribution-diff-group" ] (List.map (viewDiffLine projectRef diffBranches) lines) + + +viewNamespaceLine : ProjectRef -> DiffBranches -> { name : FQN.FQN, lines : List Diff.DiffLine } -> ( String, List (Html Msg) ) +viewNamespaceLine projectRef diffBranches { name, lines } = + ( "namespace" + , [ div [ class "namespace-info" ] [ Icon.view Icon.folder, FQN.view name ] + , viewContributionDiffGroup projectRef diffBranches lines + ] + ) + + +viewDiff : AppContext -> ProjectRef -> Diff -> Html Msg +viewDiff _ projectRef diff = + let + summary = + Diff.summary diff.lines + + diffBranches = + { oldBranch = diff.oldBranch + , newBranch = diff.newBranch + } + in + Card.card + [ h2 [] + [ text (pluralize "change" "changes" summary.numChanges) + , text " across " + , text (pluralize "namespace" "namespaces" summary.numNamespaceChanges) + ] + , Divider.divider |> Divider.small |> Divider.withoutMargin |> Divider.view + , viewContributionDiffGroup projectRef diffBranches diff.lines + ] + |> Card.withClassName "changes" + |> Card.asContained + |> Card.view + + +viewLoadingPage : PageContent Msg +viewLoadingPage = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + in + PageContent.oneColumn + [ div [] + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + ] + + +viewErrorPage : ContributionRef -> Http.Error -> PageContent Msg +viewErrorPage _ _ = + PageContent.oneColumn + [ StatusBanner.bad "Something broke on our end and we couldn't show the contribution changes. Please try again." + ] + + +view : AppContext -> ProjectRef -> Contribution -> Model -> PageContent Msg +view appContext projectRef contribution model = + case model.diff of + NotAsked -> + viewLoadingPage + + Loading -> + viewLoadingPage + + Success diff -> + PageContent.oneColumn + [ TabList.tabList + [ TabList.tab "Overview" (Link.projectContribution projectRef contribution.ref) + ] + (TabList.tab "Changes" (Link.projectContributionChanges projectRef contribution.ref)) + [] + |> TabList.view + , div [ class "project-contribution-changes-page" ] [ viewDiff appContext projectRef diff ] + ] + + Failure e -> + viewErrorPage contribution.ref e diff --git a/src/UnisonShare/Page/ProjectContributionOverviewPage.elm b/src/UnisonShare/Page/ProjectContributionOverviewPage.elm new file mode 100644 index 00000000..f7050061 --- /dev/null +++ b/src/UnisonShare/Page/ProjectContributionOverviewPage.elm @@ -0,0 +1,442 @@ +module UnisonShare.Page.ProjectContributionOverviewPage exposing (..) + +import Code.BranchRef as BranchRef +import Html exposing (Html, div, em, header, p, text) +import Html.Attributes exposing (class) +import Http +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Markdown +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.ByAt as ByAt +import UI.Card as Card +import UI.CopyField as CopyField +import UI.DateTime as DateTime +import UI.Icon as Icon +import UI.Modal as Modal +import UI.PageContent as PageContent exposing (PageContent) +import UI.TabList as TabList +import UI.Tooltip as Tooltip +import UnisonShare.Account as Account +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Contribution as Contribution exposing (Contribution) +import UnisonShare.Contribution.ContributionEvent as ContributionEvent +import UnisonShare.Contribution.ContributionRef exposing (ContributionRef) +import UnisonShare.Contribution.ContributionStatus exposing (ContributionStatus(..)) +import UnisonShare.ContributionTimeline as ContributionTimeline +import UnisonShare.DateTimeContext exposing (DateTimeContext) +import UnisonShare.Link as Link +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Session as Session exposing (Session) +import UnisonShare.Timeline.TimelineEvent as TimelineEvent + + + +-- MODEL + + +type UpdateStatus + = TimelineNotReady + | Idle + | UpdatingStatus + | UpdateStatusFailed Http.Error + + +type ContributionOverviewModal + = NoModal + | HowToReviewModal + + +type alias Model = + { timeline : ContributionTimeline.Model + , updateStatus : UpdateStatus + , modal : ContributionOverviewModal + } + + +init : AppContext -> ProjectRef -> ContributionRef -> ( Model, Cmd Msg ) +init appContext projectRef contribRef = + let + ( timeline, timelineCmd ) = + ContributionTimeline.init appContext projectRef contribRef + in + ( { timeline = timeline + , updateStatus = Idle + , modal = NoModal + } + , Cmd.map ContributionTimelineMsg timelineCmd + ) + + + +-- UPDATE + + +type Msg + = NoOp + | UpdateStatus ContributionStatus + | UpdateStatusFinished ContributionStatus (HttpResult ()) + | ShowHowToReviewModal + | CloseModal + | ContributionTimelineMsg ContributionTimeline.Msg + + +type OutMsg + = NoOut + | ContributionStatusUpdated ContributionStatus + + +update : AppContext -> ProjectRef -> ContributionRef -> WebData Contribution -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef contributionRef contribution msg model = + case msg of + NoOp -> + ( model, Cmd.none, NoOut ) + + UpdateStatus newStatus -> + ( { model | updateStatus = UpdatingStatus } + , updateContributionStatus appContext projectRef contributionRef newStatus + , NoOut + ) + + UpdateStatusFinished newStatus res -> + case appContext.session of + Session.SignedIn me -> + case ( res, contribution ) of + ( Ok _, Success contrib ) -> + let + contributionEvent = + ContributionEvent.StatusChange + { newStatus = newStatus + , oldStatus = Just contrib.status + , timestamp = appContext.now + , actor = Account.toUserSummary me + } + in + ( { model + | timeline = ContributionTimeline.addEvent model.timeline contributionEvent + , updateStatus = Idle + } + , Cmd.none + , ContributionStatusUpdated newStatus + ) + + ( Err e, _ ) -> + ( { model | updateStatus = UpdateStatusFailed e }, Cmd.none, NoOut ) + + _ -> + ( model, Cmd.none, NoOut ) + + Session.Anonymous -> + ( model, Cmd.none, NoOut ) + + ShowHowToReviewModal -> + ( { model | modal = HowToReviewModal }, Cmd.none, NoOut ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none, NoOut ) + + ContributionTimelineMsg timelineMsg -> + let + ( timeline, timelineCmd ) = + ContributionTimeline.update appContext projectRef contributionRef timelineMsg model.timeline + in + ( { model | timeline = timeline }, Cmd.map ContributionTimelineMsg timelineCmd, NoOut ) + + + +-- EFFECTS + + +updateContributionStatus : + AppContext + -> ProjectRef + -> ContributionRef + -> ContributionStatus + -> Cmd Msg +updateContributionStatus appContext projectRef contributionRef newStatus = + let + update_ = + ShareApi.ProjectContributionStatusUpdate newStatus + in + ShareApi.updateProjectContribution projectRef contributionRef update_ + |> HttpApi.toRequestWithEmptyResponse (UpdateStatusFinished newStatus) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewContribution : Session -> ProjectRef -> UpdateStatus -> Contribution -> Html Msg +viewContribution session projectRef updateStatus contribution = + let + isContributor = + contribution.authorHandle + |> Maybe.map (\h -> Session.isHandle h session) + |> Maybe.withDefault False + + hasProjectAccess = + Session.hasProjectAccess projectRef session + + className = + if updateStatus == UpdatingStatus then + "contribution-description contribution-description_updating" + + else + "contribution-description" + + description = + contribution.description + |> Maybe.map (Markdown.toHtml [ class "definition-doc" ]) + |> Maybe.withDefault (em [ class "no-description" ] [ text "No description..." ]) + + browseButton = + Button.iconThenLabel_ + (Link.projectBranchRoot contribution.projectRef contribution.sourceBranchRef) + Icon.browse + "Browse Code" + |> Button.view + + reviewButton = + Button.iconThenLabel + ShowHowToReviewModal + Icon.questionmark + "How to review contribution code?" + |> Button.subdued + |> Button.small + |> Button.view + + archiveButton = + if (hasProjectAccess || isContributor) && updateStatus /= TimelineNotReady then + Button.iconThenLabel (UpdateStatus Archived) Icon.archive "Archive" + |> Button.outlined + |> Button.view + + else + UI.nothing + + mergeButton = + if hasProjectAccess && updateStatus /= TimelineNotReady then + let + markAsMergedTooltip = + Tooltip.tooltip (Tooltip.text "We currently don't support automatic merging.\nPlease merge manually before marking the contribution as merged.") + |> Tooltip.withArrow Tooltip.End + in + Tooltip.view + (Button.iconThenLabel (UpdateStatus Merged) Icon.merge "Mark as Merged" + |> Button.positive + |> Button.view + ) + markAsMergedTooltip + + else + UI.nothing + + reopenButton = + if hasProjectAccess || isContributor then + Button.iconThenLabel (UpdateStatus InReview) Icon.conversation "Re-open" + |> Button.outlined + |> Button.view + + else + UI.nothing + + actions = + case contribution.status of + Draft -> + [ browseButton + , div [ class "right-actions" ] + [ Button.iconThenLabel (UpdateStatus InReview) Icon.conversation "Submit for review" + |> Button.emphasized + |> Button.view + ] + ] + + InReview -> + [ div [ class "left-actions" ] [ browseButton, reviewButton ] + , div [ class "right-actions" ] [ archiveButton, mergeButton ] + ] + + Merged -> + [ browseButton ] + + Archived -> + [ browseButton + , div [ class "right-actions" ] [ reopenButton ] + ] + + actions_ = + if List.isEmpty actions then + UI.nothing + + else + div [ class "actions" ] actions + in + Card.card + [ description, actions_ ] + |> Card.asContained + |> Card.withClassName className + |> Card.view + + +viewStatusChangeEvent : DateTimeContext a -> ContributionEvent.StatusChangeDetails -> List (Html Msg) +viewStatusChangeEvent dtContext { newStatus, oldStatus, actor, timestamp } = + let + byAt = + ByAt.byAt actor timestamp + |> ByAt.view dtContext.timeZone dtContext.now + in + case newStatus of + Draft -> + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.writingPad + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle "Created Draft" ] + , byAt + ] + ] + ] + + InReview -> + let + title = + case oldStatus of + Just Archived -> + "Re-opened" + + _ -> + "Submitted for review" + in + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.conversation + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle title ] + , byAt + ] + ] + ] + + Merged -> + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.merge + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle "Merged" ] + , byAt + ] + ] + ] + + Archived -> + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.archive + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle "Archived" ] + , byAt + ] + ] + ] + + +viewPageContent : + AppContext + -> ProjectRef + -> UpdateStatus + -> Contribution + -> ContributionTimeline.Model + -> PageContent Msg +viewPageContent appContext projectRef updateStatus contribution timeline = + let + timeline_ = + ContributionTimeline.view appContext projectRef timeline + + tabs = + -- Before this date, we couldn't show diffs on merged + -- contributions, so we don't want to show the "changes" tab + if DateTime.isAfter Contribution.dateOfHistoricDiffSupport contribution.createdAt || contribution.status == InReview then + TabList.tabList + [] + (TabList.tab "Overview" (Link.projectContribution projectRef contribution.ref)) + [ TabList.tab "Changes" (Link.projectContributionChanges projectRef contribution.ref) ] + |> TabList.view + + else + UI.nothing + in + PageContent.oneColumn + [ tabs + , div [ class "project-contribution-overview-page" ] + [ viewContribution appContext.session projectRef updateStatus contribution + , Html.map ContributionTimelineMsg timeline_ + ] + ] + + +viewHowToReviewModal : Contribution -> Html Msg +viewHowToReviewModal contribution = + let + projectRef = + ProjectRef.toString contribution.projectRef + + source = + "/" ++ BranchRef.toString contribution.sourceBranchRef + + target = + "/" ++ BranchRef.toString contribution.targetBranchRef + + content = + div [] + [ p [] [ text "Reviewing and merging contribution code is a manual process for now. Follow the steps below." ] + , div [ class "instructions" ] + [ p [] [ text "From within the project clone the source branch:" ] + , CopyField.copyField (always NoOp) ("clone " ++ source) + |> CopyField.withPrefix (projectRef ++ "/main>") + |> CopyField.view + , p [] [ text "Next, switch to the target branch:" ] + , CopyField.copyField (always NoOp) ("switch " ++ target) + |> CopyField.withPrefix (projectRef ++ source ++ ">") + |> CopyField.view + , p [] [ text "See a preview with the changes:" ] + , CopyField.copyField (always NoOp) ("merge.preview " ++ source) + |> CopyField.withPrefix (projectRef ++ target ++ ">") + |> CopyField.view + , p [] [ text "Merge the changes to accept the contribution:" ] + , CopyField.copyField (always NoOp) ("merge " ++ source) + |> CopyField.withPrefix (projectRef ++ target ++ ">") + |> CopyField.view + , p [] [ text "Finally, push the project to share and mark the contribution as merged." ] + ] + ] + in + content + |> Modal.content + |> Modal.modal "project-contribution-how-to-review-modal" CloseModal + |> Modal.withActions + [ Button.button CloseModal "Got it" + |> Button.medium + |> Button.emphasized + ] + |> Modal.withHeader "How to review contribution code?" + |> Modal.view + + +view : AppContext -> ProjectRef -> Contribution -> Model -> ( PageContent Msg, Maybe (Html Msg) ) +view appContext projectRef contribution model = + let + modal = + case model.modal of + HowToReviewModal -> + Just (viewHowToReviewModal contribution) + + _ -> + Nothing + in + ( viewPageContent + appContext + projectRef + model.updateStatus + contribution + model.timeline + , modal + ) diff --git a/src/UnisonShare/Page/ProjectContributionPage.elm b/src/UnisonShare/Page/ProjectContributionPage.elm new file mode 100644 index 00000000..90dd1e0a --- /dev/null +++ b/src/UnisonShare/Page/ProjectContributionPage.elm @@ -0,0 +1,429 @@ +module UnisonShare.Page.ProjectContributionPage exposing (..) + +import Code.BranchRef as BranchRef +import Html exposing (Html, div, h1, span, text) +import Html.Attributes exposing (class) +import Http +import Lib.HttpApi as HttpApi +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.ByAt as ByAt +import UI.Card as Card +import UI.DateTime as DateTime exposing (DateTime) +import UI.Icon as Icon +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle exposing (PageTitle) +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UI.Tag as Tag +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Contribution as Contribution exposing (Contribution) +import UnisonShare.Contribution.ContributionRef as ContributionRef exposing (ContributionRef) +import UnisonShare.DateTimeContext exposing (DateTimeContext) +import UnisonShare.Link as Link +import UnisonShare.Page.ProjectContributionChangesPage as ProjectContributionChangesPage +import UnisonShare.Page.ProjectContributionOverviewPage as ProjectContributionOverviewPage +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.ProjectContributionFormModal as ProjectContributionFormModal +import UnisonShare.Route exposing (ProjectContributionRoute(..)) +import UnisonShare.Session as Session + + + +-- MODEL + + +type ContributionModal + = NoModal + | EditModal ProjectContributionFormModal.Model + + +type ProjectContributionSubPage + = Overview ProjectContributionOverviewPage.Model + | Changes ProjectContributionChangesPage.Model + + +type alias Model = + { contribution : WebData Contribution + , modal : ContributionModal + , subPage : ProjectContributionSubPage + } + + +init : AppContext -> ProjectRef -> ContributionRef -> ProjectContributionRoute -> ( Model, Cmd Msg ) +init appContext projectRef contribRef route = + let + ( subPage, subPageCmd ) = + case route of + ProjectContributionOverview -> + let + ( overviewPage, overviewPageCmd ) = + ProjectContributionOverviewPage.init appContext projectRef contribRef + in + ( Overview overviewPage, Cmd.map ProjectContributionOverviewPageMsg overviewPageCmd ) + + ProjectContributionChanges -> + let + ( changesPage, changesPageCmd ) = + ProjectContributionChangesPage.init appContext projectRef contribRef + in + ( Changes changesPage, Cmd.map ProjectContributionChangesPageMsg changesPageCmd ) + in + ( { contribution = Loading + , modal = NoModal + , subPage = subPage + } + , Cmd.batch + [ fetchContribution appContext projectRef contribRef + , subPageCmd + ] + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchContributionFinished (WebData Contribution) + | ShowEditModal + | ProjectContributionFormModalMsg ProjectContributionFormModal.Msg + | CloseModal + | ProjectContributionOverviewPageMsg ProjectContributionOverviewPage.Msg + | ProjectContributionChangesPageMsg ProjectContributionChangesPage.Msg + + +update : + AppContext + -> ProjectRef + -> ContributionRef + -> ProjectContributionRoute + -> Msg + -> Model + -> ( Model, Cmd Msg ) +update appContext projectRef contribRef _ msg model = + case ( model.subPage, msg ) of + ( _, NoOp ) -> + ( model, Cmd.none ) + + ( _, FetchContributionFinished contrib ) -> + ( { model | contribution = contrib }, Cmd.none ) + + ( _, ShowEditModal ) -> + case ( appContext.session, model.contribution ) of + ( Session.SignedIn a, Success contrib ) -> + let + ( formModel, formCmd ) = + ProjectContributionFormModal.init appContext + a + projectRef + (ProjectContributionFormModal.Edit contrib) + in + ( { model | modal = EditModal formModel }, Cmd.map ProjectContributionFormModalMsg formCmd ) + + _ -> + ( model, Cmd.none ) + + ( _, ProjectContributionFormModalMsg formMsg ) -> + case ( appContext.session, model.modal ) of + ( Session.SignedIn account, EditModal formModel ) -> + let + ( projectContributionFormModal, cmd, out ) = + ProjectContributionFormModal.update appContext + projectRef + account + formMsg + formModel + + ( modal, contribution ) = + case out of + ProjectContributionFormModal.None -> + ( EditModal projectContributionFormModal, model.contribution ) + + ProjectContributionFormModal.RequestToCloseModal -> + ( NoModal, model.contribution ) + + ProjectContributionFormModal.Saved c -> + -- TODO: also add a ContributionEvent + ( NoModal, Success c ) + in + ( { model | modal = modal, contribution = contribution } + , Cmd.map ProjectContributionFormModalMsg cmd + ) + + _ -> + ( model, Cmd.none ) + + ( _, CloseModal ) -> + ( { model | modal = NoModal }, Cmd.none ) + + ( Overview overviewPage, ProjectContributionOverviewPageMsg overviewPageMsg ) -> + let + ( overviewPage_, overviewPageCmd, outMsg ) = + ProjectContributionOverviewPage.update appContext + projectRef + contribRef + model.contribution + overviewPageMsg + overviewPage + + contrib = + case outMsg of + ProjectContributionOverviewPage.NoOut -> + model.contribution + + ProjectContributionOverviewPage.ContributionStatusUpdated status -> + RemoteData.map (\c -> { c | status = status }) model.contribution + in + ( { model + | contribution = contrib + , subPage = Overview overviewPage_ + } + , Cmd.map ProjectContributionOverviewPageMsg overviewPageCmd + ) + + ( Changes changesPage, ProjectContributionChangesPageMsg changesPageMsg ) -> + let + ( changesPage_, changesPageCmd ) = + ProjectContributionChangesPage.update appContext + projectRef + contribRef + changesPageMsg + changesPage + in + ( { model | subPage = Changes changesPage_ } + , Cmd.map ProjectContributionChangesPageMsg changesPageCmd + ) + + _ -> + ( model, Cmd.none ) + + +updateSubPage : AppContext -> ProjectRef -> ContributionRef -> ProjectContributionRoute -> Model -> ( Model, Cmd Msg ) +updateSubPage appContext projectRef contribRef contribRoute model = + case contribRoute of + ProjectContributionOverview -> + case model.subPage of + Overview _ -> + ( model, Cmd.none ) + + _ -> + let + ( overviewPage, overviewPageCmd ) = + ProjectContributionOverviewPage.init appContext projectRef contribRef + in + ( { model | subPage = Overview overviewPage }, Cmd.map ProjectContributionOverviewPageMsg overviewPageCmd ) + + ProjectContributionChanges -> + case model.subPage of + Changes _ -> + ( model, Cmd.none ) + + _ -> + let + ( changesPage, changesPageCmd ) = + ProjectContributionChangesPage.init appContext projectRef contribRef + in + ( { model | subPage = Changes changesPage }, Cmd.map ProjectContributionChangesPageMsg changesPageCmd ) + + + +-- EFFECTS + + +fetchContribution : AppContext -> ProjectRef -> ContributionRef -> Cmd Msg +fetchContribution appContext projectRef contributionRef = + ShareApi.projectContribution projectRef contributionRef + |> HttpApi.toRequest Contribution.decode (RemoteData.fromResult >> FetchContributionFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewPageContent : + AppContext + -> ProjectRef + -> Contribution + -> ProjectContributionSubPage + -> ( PageContent Msg, Maybe (Html Msg) ) +viewPageContent appContext projectRef contribution subPage = + let + pageTitle_ = + detailedPageTitle appContext contribution + in + case subPage of + Overview overview -> + let + ( overviewPage, modal ) = + ProjectContributionOverviewPage.view appContext projectRef contribution overview + in + ( PageContent.map ProjectContributionOverviewPageMsg overviewPage + |> PageContent.withPageTitle pageTitle_ + , Maybe.map (Html.map ProjectContributionOverviewPageMsg) modal + ) + + Changes changes -> + let + changesPage = + ProjectContributionChangesPage.view appContext projectRef contribution changes + in + ( PageContent.map ProjectContributionChangesPageMsg changesPage + |> PageContent.withPageTitle pageTitle_ + , Nothing + ) + + +timeAgo : DateTimeContext a -> DateTime -> Html msg +timeAgo dateTimeContext t = + DateTime.view (DateTime.DistanceFrom dateTimeContext.now) dateTimeContext.timeZone t + + +detailedPageTitle : AppContext -> Contribution -> PageTitle Msg +detailedPageTitle appContext contribution = + let + isContributor = + contribution.authorHandle + |> Maybe.map (\h -> Session.isHandle h appContext.session) + |> Maybe.withDefault False + + editButton = + if isContributor then + Button.iconThenLabel ShowEditModal Icon.writingPad "Edit" + |> Button.small + |> Button.outlined + |> Button.view + + else + UI.nothing + + byAt = + contribution.authorHandle + |> Maybe.map (\h -> ByAt.handleOnly h contribution.createdAt) + |> Maybe.withDefault (ByAt.byUnknown contribution.createdAt) + |> ByAt.view appContext.timeZone appContext.now + in + PageTitle.custom + [ div [ class "contribution-page-title" ] + [ div [ class "page-title_pre-title" ] + [ span [ class "contribution-ref_by-at" ] + [ span [ class "contribution-ref" ] [ text (ContributionRef.toString contribution.ref) ] + , byAt + ] + , editButton + ] + , h1 [] [ text contribution.title ] + , div [ class "page-title_description" ] + [ span + [ class "from-to" ] + [ text "from" + , span [ class "branches" ] + [ BranchRef.toTag contribution.sourceBranchRef + |> Tag.withClick (Link.projectBranchRoot contribution.projectRef contribution.sourceBranchRef) + |> Tag.large + |> Tag.view + , text "to" + , BranchRef.toTag contribution.targetBranchRef + |> Tag.withClick (Link.projectBranchRoot contribution.projectRef contribution.targetBranchRef) + |> Tag.large + |> Tag.view + ] + ] + ] + ] + ] + + +viewLoadingPage : PageLayout Msg +viewLoadingPage = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ div [] + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + ] + |> PageContent.withPageTitle (PageTitle.title "Loading") + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewErrorPage : ContributionRef -> Http.Error -> PageLayout Msg +viewErrorPage _ _ = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn + [ Card.card + [ StatusBanner.bad "Something broke on our end and we couldn't show the contribution. Please try again." + ] + |> Card.withClassName "project-contribution_error" + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (PageTitle.title "Error") + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +view : AppContext -> ProjectRef -> ContributionRef -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext projectRef contribRef model = + case model.contribution of + NotAsked -> + ( viewLoadingPage, Nothing ) + + Loading -> + ( viewLoadingPage, Nothing ) + + Success contribution -> + let + ( pageContent, modal_ ) = + viewPageContent + appContext + projectRef + contribution + model.subPage + + modal = + case model.modal of + EditModal form -> + Just + (Html.map ProjectContributionFormModalMsg + (ProjectContributionFormModal.view + projectRef + "Save Contribution" + form + ) + ) + + _ -> + modal_ + in + ( PageLayout.centeredNarrowLayout + pageContent + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Failure e -> + ( viewErrorPage contribRef e, Nothing ) diff --git a/src/UnisonShare/Page/ProjectContributionsPage.elm b/src/UnisonShare/Page/ProjectContributionsPage.elm new file mode 100644 index 00000000..98659486 --- /dev/null +++ b/src/UnisonShare/Page/ProjectContributionsPage.elm @@ -0,0 +1,433 @@ +module UnisonShare.Page.ProjectContributionsPage exposing (..) + +import Code.BranchRef as BranchRef +import Html exposing (Html, div, h2, header, span, text) +import Html.Attributes exposing (class) +import Json.Decode as Decode +import Lib.HttpApi as HttpApi +import Maybe.Extra as MaybeE +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.ByAt as ByAt +import UI.Card as Card +import UI.Click as Click +import UI.Divider as Divider +import UI.EmptyState as EmptyState +import UI.EmptyStateCard as EmptyStateCard +import UI.Icon as Icon +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle +import UI.Placeholder as Placeholder +import UI.TabList as TabList +import UI.Tag as Tag +import UI.Tooltip as Tooltip +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.BranchSummary as BranchSummary exposing (BranchSummary) +import UnisonShare.Contribution as Contribution exposing (Contribution) +import UnisonShare.Contribution.ContributionRef as ContributionRef +import UnisonShare.Contribution.ContributionStatus as ContributionStatus +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.ProjectContributionFormModal as ProjectContributionFormModal +import UnisonShare.Session as Session exposing (Session) + + + +-- MODEL + + +type ContribitionsModal + = NoModal + | SubmitContributionModal ProjectContributionFormModal.Model + + +type Tab + = InReview + | Merged + | Archived + + +type alias RecentBranches = + { ownContributorBranches : WebData (List BranchSummary) + , projectBranches : WebData (List BranchSummary) + } + + +type alias Model = + { contributions : WebData (List Contribution) + , modal : ContribitionsModal + , tab : Tab + , recentBranches : Maybe RecentBranches + } + + +init : AppContext -> ProjectRef -> ( Model, Cmd Msg ) +init appContext projectRef = + let + ( recentBranches, recentBranchesCmds ) = + case appContext.session of + Session.SignedIn a -> + ( Just { ownContributorBranches = Loading, projectBranches = Loading } + , [ fetchBranches FetchOwnContributorBranchesFinished + appContext + projectRef + { kind = ShareApi.ContributorBranches (Just a.handle) + , searchQuery = Nothing + , limit = 3 + , cursor = Nothing + } + , fetchBranches FetchProjectBranchesFinished + appContext + projectRef + { kind = ShareApi.ProjectBranches + , searchQuery = Nothing + , limit = 3 + , cursor = Nothing + } + ] + ) + + Session.Anonymous -> + ( Nothing, [] ) + in + ( { contributions = Loading + , modal = NoModal + , tab = InReview + , recentBranches = recentBranches + } + , Cmd.batch + (fetchProjectContributions appContext projectRef :: recentBranchesCmds) + ) + + + +-- UPDATE + + +type Msg + = FetchContributionsFinished (WebData (List Contribution)) + | FetchOwnContributorBranchesFinished (WebData (List BranchSummary)) + | FetchProjectBranchesFinished (WebData (List BranchSummary)) + | ShowSubmitContributionModal + | ProjectContributionFormModalMsg ProjectContributionFormModal.Msg + | CloseModal + | ChangeTab Tab + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef msg model = + case msg of + FetchContributionsFinished contributions -> + ( { model | contributions = contributions }, Cmd.none ) + + FetchOwnContributorBranchesFinished contribBranches -> + let + recentBranches = + model.recentBranches + |> Maybe.map (\rb -> { rb | ownContributorBranches = contribBranches }) + in + ( { model | recentBranches = recentBranches }, Cmd.none ) + + FetchProjectBranchesFinished projectBranches -> + let + recentBranches = + model.recentBranches + |> Maybe.map (\rb -> { rb | projectBranches = projectBranches }) + in + ( { model | recentBranches = recentBranches }, Cmd.none ) + + ShowSubmitContributionModal -> + case appContext.session of + Session.SignedIn a -> + let + ( projectContributionFormModal, cmd ) = + ProjectContributionFormModal.init + appContext + a + projectRef + ProjectContributionFormModal.Create + in + ( { model | modal = SubmitContributionModal projectContributionFormModal } + , Cmd.map ProjectContributionFormModalMsg cmd + ) + + Session.Anonymous -> + ( model, Cmd.none ) + + ProjectContributionFormModalMsg formMsg -> + case ( appContext.session, model.modal ) of + ( Session.SignedIn account, SubmitContributionModal formModel ) -> + let + ( projectContributionFormModal, cmd, out ) = + ProjectContributionFormModal.update appContext projectRef account formMsg formModel + + ( modal, contributions ) = + case out of + ProjectContributionFormModal.None -> + ( SubmitContributionModal projectContributionFormModal, model.contributions ) + + ProjectContributionFormModal.RequestToCloseModal -> + ( NoModal, model.contributions ) + + ProjectContributionFormModal.Saved c -> + ( NoModal, RemoteData.map (\cs -> c :: cs) model.contributions ) + in + ( { model | modal = modal, contributions = contributions } + , Cmd.map ProjectContributionFormModalMsg cmd + ) + + _ -> + ( model, Cmd.none ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none ) + + ChangeTab t -> + ( { model | tab = t }, Cmd.none ) + + + +-- EFFECTS + + +fetchProjectContributions : AppContext -> ProjectRef -> Cmd Msg +fetchProjectContributions appContext projectRef = + ShareApi.projectContributions projectRef + |> HttpApi.toRequest + (Decode.field "items" (Decode.list Contribution.decode)) + (RemoteData.fromResult >> FetchContributionsFinished) + |> HttpApi.perform appContext.api + + +fetchBranches : + (WebData (List BranchSummary) -> Msg) + -> AppContext + -> ProjectRef + -> ShareApi.ProjectBranchesParams + -> Cmd Msg +fetchBranches doneMsg appContext projectRef params = + ShareApi.projectBranches projectRef params + |> HttpApi.toRequest + (Decode.field "items" (Decode.list BranchSummary.decode)) + (RemoteData.fromResult >> doneMsg) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewPageTitle : Session -> ProjectRef -> Maybe RecentBranches -> PageTitle.PageTitle Msg +viewPageTitle session projectRef recentBranches = + let + pt = + PageTitle.title "Contributions" + + hasRecentContributorBranches = + MaybeE.unwrap + False + (.ownContributorBranches >> RemoteData.map (List.isEmpty >> not) >> RemoteData.withDefault False) + recentBranches + + button = + Button.iconThenLabel ShowSubmitContributionModal Icon.merge "Submit contribution" + in + if Session.hasProjectAccess projectRef session || hasRecentContributorBranches then + pt + |> PageTitle.withRightSide + [ button + |> Button.emphasized + |> Button.view + ] + + else + pt + |> PageTitle.withRightSide + [ div [ class "submit-contribution-disabled" ] + [ Tooltip.text "Create a Contribution by pushing a feature branch to Share." + |> Tooltip.tooltip + |> Tooltip.view + (button + |> Button.disabled + |> Button.view + ) + ] + ] + + +viewLoadingPage : PageLayout msg +viewLoadingPage = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (PageTitle.title "Contributions") + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewContributionRow : AppContext -> ProjectRef -> Contribution -> Html Msg +viewContributionRow appContext projectRef contribution = + let + byAt = + case contribution.authorHandle of + Just h -> + ByAt.handleOnly h contribution.createdAt + + Nothing -> + ByAt.byUnknown contribution.createdAt + + numComments = + if contribution.numComments > 0 then + div [ class "num-comments" ] + [ Icon.view Icon.conversation + , text (String.fromInt contribution.numComments) + ] + + else + UI.nothing + in + div [ class "contribution-row" ] + [ header [ class "contribution-row_header" ] + [ Click.view [] + [ h2 [] + [ span [ class "contribution-row_ref" ] + [ text (ContributionRef.toString contribution.ref) + ] + , text contribution.title + ] + ] + (Link.projectContribution projectRef contribution.ref) + , numComments + ] + , div [ class "contribution-row_info" ] + [ ByAt.view appContext.timeZone appContext.now byAt + , Tag.view (BranchRef.toTag contribution.sourceBranchRef) + ] + ] + + +viewPageContent : AppContext -> ProjectRef -> Maybe RecentBranches -> Tab -> List Contribution -> PageContent Msg +viewPageContent appContext projectRef recentBranches tab contributions = + let + viewEmptyState icon text_ = + EmptyState.iconCloud + (EmptyState.CircleCenterPiece + (div [ class "contributions-empty-state_icon" ] [ Icon.view icon ]) + ) + |> EmptyState.withContent [ h2 [] [ text text_ ] ] + |> EmptyStateCard.view + + ( tabList, status, emptyState ) = + case tab of + InReview -> + ( TabList.tabList [] + (TabList.tab "In Review" (Click.onClick (ChangeTab InReview))) + [ TabList.tab "Merged" (Click.onClick (ChangeTab Merged)) + , TabList.tab "Archived" (Click.onClick (ChangeTab Archived)) + ] + , ContributionStatus.InReview + , viewEmptyState Icon.conversation "There are currently no open contributions in review." + ) + + Merged -> + ( TabList.tabList + [ TabList.tab "In Review" (Click.onClick (ChangeTab InReview)) ] + (TabList.tab "Merged" (Click.onClick (ChangeTab Merged))) + [ TabList.tab "Archived" (Click.onClick (ChangeTab Archived)) ] + , ContributionStatus.Merged + , viewEmptyState Icon.merge "There are currently no merged contributions." + ) + + Archived -> + ( TabList.tabList + [ TabList.tab "In Review" (Click.onClick (ChangeTab InReview)) + , TabList.tab "Merged" (Click.onClick (ChangeTab Merged)) + ] + (TabList.tab "Archived" (Click.onClick (ChangeTab Archived))) + [] + , ContributionStatus.Archived + , viewEmptyState Icon.archive "There are currently no archived contributions." + ) + + divider = + Divider.divider + |> Divider.small + |> Divider.withoutMargin + + content = + contributions + |> List.filter (\c -> c.status == status) + |> List.map (viewContributionRow appContext projectRef) + |> List.intersperse (Divider.view divider) + + card = + if List.isEmpty content then + emptyState + + else + Card.card content + |> Card.withClassName "project-contributions" + |> Card.asContained + |> Card.view + in + PageContent.oneColumn [ TabList.view tabList, card ] + |> PageContent.withPageTitle (viewPageTitle appContext.session projectRef recentBranches) + + +view : AppContext -> ProjectRef -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext projectRef model = + case model.contributions of + NotAsked -> + ( viewLoadingPage, Nothing ) + + Loading -> + ( viewLoadingPage, Nothing ) + + Success contributions -> + let + modal = + case ( model.modal, model.recentBranches ) of + ( SubmitContributionModal form, Just _ ) -> + Just + (Html.map ProjectContributionFormModalMsg + (ProjectContributionFormModal.view projectRef "Submit contribution for review" form) + ) + + _ -> + Nothing + in + ( PageLayout.centeredNarrowLayout + (viewPageContent appContext projectRef model.recentBranches model.tab contributions) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Failure _ -> + -- TODO + ( PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ text "Couldn't load contributions..." ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , Nothing + ) diff --git a/src/UnisonShare/Page/ProjectOverviewPage.elm b/src/UnisonShare/Page/ProjectOverviewPage.elm new file mode 100644 index 00000000..e48c0ca2 --- /dev/null +++ b/src/UnisonShare/Page/ProjectOverviewPage.elm @@ -0,0 +1,893 @@ +module UnisonShare.Page.ProjectOverviewPage exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.Config exposing (Config) +import Code.Definition.Readme as Readme exposing (Readme) +import Code.Finder as Finder +import Code.Finder.SearchOptions as SearchOptions +import Code.FullyQualifiedName as FQN +import Code.Namespace.NamespaceRef as NamespaceRef +import Code.Perspective as Perspective +import Code.ReadmeCard as ReadmeCard +import Html exposing (Html, div, footer, form, p, span, strong, text) +import Html.Attributes exposing (class) +import Http exposing (Error) +import Json.Decode as Decode +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (requiredAt) +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util exposing (unicodeStringLength) +import Maybe.Extra as MaybeE +import RemoteData exposing (RemoteData(..), WebData) +import Set exposing (Set) +import UI +import UI.Button as Button +import UI.Card as Card +import UI.Click as Click +import UI.CopyField as CopyField +import UI.EmptyState as EmptyState +import UI.EmptyStateCard as EmptyStateCard +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.KeyboardShortcut as KeyboardShortcut +import UI.KeyboardShortcut.Key exposing (Key(..)) +import UI.KeyboardShortcut.KeyboardEvent as KeyboardEvent exposing (KeyboardEvent) +import UI.KpiTag as KpiTag +import UI.Modal as Modal +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UI.StatusIndicator as StatusIndicator +import UI.Steps as Steps +import UI.Tag as Tag +import UI.Tooltip as Tooltip +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext as AppContext exposing (AppContext) +import UnisonShare.CodeBrowsingContext as CodeBrowsingContext exposing (CodeBrowsingContext(..)) +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project as Project exposing (ProjectDetails) +import UnisonShare.Project.ProjectDependency as ProjectDependency exposing (ProjectDependency) +import UnisonShare.Project.ProjectListing as ProjectListing +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Route as Route +import UnisonShare.Session as Session exposing (Session) + + + +-- MODEL + + +{-| these are formfield values, so they are less strict than their equivalent +Project fields; `tags`, for instance, is a string separated by space as opposed +to `Set String`. +-} +type alias EditDescriptionFormFields = + { summary : String, tags : String } + + +type EditDescriptionForm + = Editing EditDescriptionFormFields + | Saving EditDescriptionFormFields + | SaveSuccessful + | SaveFailed EditDescriptionFormFields Error + + +type ProjectOverviewModal + = NoModal + | FinderModal Finder.Model + | ReadmeInstructionsModal + | EditDescriptionModal EditDescriptionForm + + +type alias Model = + { readme : WebData ( Maybe Readme, ReadmeCard.Model ) + , modal : ProjectOverviewModal + , keyboardShortcut : KeyboardShortcut.Model + , dependencies : WebData (List ProjectDependency) + } + + +init : AppContext -> ProjectRef -> ( Model, Cmd Msg ) +init appContext projectRef = + ( { readme = Loading + , modal = NoModal + , keyboardShortcut = KeyboardShortcut.init appContext.operatingSystem + , dependencies = NotAsked + } + , fetchReadme appContext projectRef + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchReadmeFinished (WebData (Maybe Readme)) + | RetryFetchReadme + | FetchDependenciesFinished (WebData (List ProjectDependency)) + | ShowReadmeInstructionsModal + | ShowEditDescriptionModal + | ShowFinderModal + | UpdateSummaryField String + | UpdateTagsField String + | SaveDescription + | SaveDescriptionFinished (HttpResult ()) + | CloseModal + | ToggleProjectFav + | ShowUseProjectModal + | Keydown KeyboardEvent + | KeyboardShortcutMsg KeyboardShortcut.Msg + | ReadmeCardMsg ReadmeCard.Msg + | FinderMsg Finder.Msg + + +type OutMsg + = None + -- The InstallModal is tracked by the ProjectPage module (since it can be + -- opened from multiple places) + | RequestToShowUseProjectModal + | RequestToToggleProjectFav + | ProjectDescriptionUpdated { summary : Maybe String, tags : Set String } + + +update : AppContext -> ProjectRef -> WebData ProjectDetails -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef project msg model = + let + config = + case project of + Success p -> + AppContext.toCodeConfig appContext + (CodeBrowsingContext.project projectRef (Project.defaultBrowsingBranch p)) + Perspective.relativeRootPerspective + + _ -> + AppContext.toCodeConfig appContext + (CodeBrowsingContext.project projectRef BranchRef.main_) + Perspective.relativeRootPerspective + in + case msg of + FetchReadmeFinished readme -> + let + readme_ = + RemoteData.map (\r -> ( r, ReadmeCard.init )) readme + in + ( { model | readme = readme_ }, Cmd.none, None ) + + RetryFetchReadme -> + ( { model | readme = Loading }, fetchReadme appContext projectRef, None ) + + FetchDependenciesFinished deps -> + ( { model | dependencies = deps }, Cmd.none, None ) + + ShowReadmeInstructionsModal -> + ( { model | modal = ReadmeInstructionsModal }, Cmd.none, None ) + + ShowEditDescriptionModal -> + case project of + Success p -> + let + form = + Editing + { summary = Maybe.withDefault "" p.summary + , tags = p.tags |> Set.toList |> String.join " " + } + in + ( { model | modal = EditDescriptionModal form }, Cmd.none, None ) + + _ -> + ( model, Cmd.none, None ) + + ShowFinderModal -> + let + ( fm, fCmd ) = + Finder.init config (SearchOptions.init config.perspective Nothing) + in + ( { model | modal = FinderModal fm }, Cmd.map FinderMsg fCmd, None ) + + UpdateSummaryField s -> + case model.modal of + EditDescriptionModal (Editing f) -> + let + form = + if unicodeStringLength s <= 100 then + Editing { f | summary = s } + + else + Editing f + in + ( { model | modal = EditDescriptionModal form }, Cmd.none, None ) + + _ -> + ( model, Cmd.none, None ) + + UpdateTagsField t -> + case model.modal of + EditDescriptionModal (Editing f) -> + let + form = + Editing { f | tags = t } + in + ( { model | modal = EditDescriptionModal form }, Cmd.none, None ) + + _ -> + ( model, Cmd.none, None ) + + SaveDescription -> + case model.modal of + EditDescriptionModal (Editing f) -> + let + modal = + EditDescriptionModal (Saving f) + in + ( { model | modal = modal } + , updateProjectDescription appContext projectRef f + , None + ) + + _ -> + ( model, Cmd.none, None ) + + SaveDescriptionFinished (Ok _) -> + case model.modal of + EditDescriptionModal (Saving f) -> + ( { model | modal = EditDescriptionModal SaveSuccessful } + , Util.delayMsg 1500 CloseModal + , ProjectDescriptionUpdated (editDescriptionFieldsToDescriptionUpdate f) + ) + + _ -> + ( model, Cmd.none, None ) + + SaveDescriptionFinished (Err e) -> + case model.modal of + EditDescriptionModal (Saving f) -> + ( { model | modal = EditDescriptionModal (SaveFailed f e) } + , Cmd.none + , None + ) + + _ -> + ( model, Cmd.none, None ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none, None ) + + ToggleProjectFav -> + ( model, Cmd.none, RequestToToggleProjectFav ) + + ShowUseProjectModal -> + ( model, Cmd.none, RequestToShowUseProjectModal ) + + Keydown event -> + keydown appContext model config event + + KeyboardShortcutMsg kMsg -> + let + ( keyboardShortcut, kCmd ) = + KeyboardShortcut.update kMsg model.keyboardShortcut + in + ( { model | keyboardShortcut = keyboardShortcut }, Cmd.map KeyboardShortcutMsg kCmd, None ) + + ReadmeCardMsg rMsg -> + case model.readme of + Success ( r, card ) -> + let + ( readmeCard, rCmd, out ) = + ReadmeCard.update config rMsg card + + browsingBranch = + case project of + Success p -> + Project.defaultBrowsingBranch p + + _ -> + BranchRef.main_ + + navCmd = + case out of + ReadmeCard.OpenDefinition ref -> + Route.navigate + appContext.navKey + (Route.projectBranchDefinition projectRef browsingBranch Perspective.relativeRootPerspective ref) + + _ -> + Cmd.none + in + ( { model | readme = Success ( r, readmeCard ) }, Cmd.batch [ Cmd.map ReadmeCardMsg rCmd, navCmd ], None ) + + _ -> + ( model, Cmd.none, None ) + + FinderMsg finderMsg -> + case model.modal of + FinderModal fm -> + let + ( fm_, fCmd, outMsg ) = + Finder.update config finderMsg fm + in + case outMsg of + Finder.Remain -> + ( { model | modal = FinderModal fm_ }, Cmd.map FinderMsg fCmd, None ) + + Finder.Exit -> + ( { model | modal = NoModal }, Cmd.map FinderMsg fCmd, None ) + + Finder.OpenDefinition r -> + let + branchRef = + case project of + Success p -> + Project.defaultBrowsingBranch p + + _ -> + BranchRef.main_ + in + ( { model | modal = NoModal } + , Cmd.batch + [ Cmd.map FinderMsg fCmd + , Route.navigate appContext.navKey + (Route.projectBranch projectRef + branchRef + (Route.definition config.perspective r) + ) + ] + , None + ) + + _ -> + ( model, Cmd.none, None ) + + NoOp -> + ( model, Cmd.none, None ) + + + +-- HELPERS + + +keydown : AppContext -> Model -> Config -> KeyboardEvent -> ( Model, Cmd Msg, OutMsg ) +keydown appContext model config keyboardEvent = + let + shortcut = + KeyboardShortcut.fromKeyboardEvent model.keyboardShortcut keyboardEvent + + noOp = + ( model, Cmd.none, None ) + in + if Finder.isShowFinderKeyboardShortcut appContext.operatingSystem shortcut then + let + ( finder, cmd ) = + Finder.init config + (SearchOptions.init config.perspective Nothing) + in + ( { model | modal = FinderModal finder }, Cmd.map FinderMsg cmd, None ) + + else + noOp + + +editDescriptionFieldsToDescriptionUpdate : EditDescriptionFormFields -> { summary : Maybe String, tags : Set String } +editDescriptionFieldsToDescriptionUpdate f = + let + summary = + if String.isEmpty f.summary then + Nothing + + else + Just f.summary + + tags = + f.tags + |> String.toLower + |> String.split " " + |> List.filter (String.isEmpty >> not) + |> Set.fromList + in + { summary = summary, tags = tags } + + + +-- EFFECTS + + +fetchReadme : AppContext -> ProjectRef -> Cmd Msg +fetchReadme appContext projectRef = + ShareApi.projectReadme projectRef + |> HttpApi.toRequest (Decode.field "readMe" (Decode.nullable Readme.decode)) + (RemoteData.fromResult >> FetchReadmeFinished) + |> HttpApi.perform appContext.api + + +fetchDependenciesAndUpdate : AppContext -> ProjectDetails -> Model -> ( Model, Cmd Msg ) +fetchDependenciesAndUpdate appContext project model = + let + branchRef = + case ( project.latestVersion, project.defaultBranch ) of + ( Just v, _ ) -> + BranchRef.ReleaseBranchRef v + + ( Nothing, Just br ) -> + br + + _ -> + BranchRef.main_ + in + ( { model | dependencies = Loading } + , fetchDependencies appContext project.ref branchRef + ) + + +fetchDependencies : AppContext -> ProjectRef -> BranchRef -> Cmd Msg +fetchDependencies appContext projectRef branchRef = + let + browsingContext = + ProjectBranch projectRef branchRef + + perspective = + Perspective.relativeRootPerspective + + namespace = + NamespaceRef.NameRef (FQN.fromString "lib") + + decodeDependency = + Decode.succeed ProjectDependency.fromString + |> requiredAt [ "contents", "namespaceName" ] Decode.string + in + ShareApi.browseCodebase browsingContext perspective (Just namespace) + |> HttpApi.toRequest (Decode.field "namespaceListingChildren" (Decode.list (when (Decode.field "tag" Decode.string) ((==) "Subnamespace") decodeDependency))) + (RemoteData.fromResult >> FetchDependenciesFinished) + |> HttpApi.perform appContext.api + + +updateProjectDescription : AppContext -> ProjectRef -> EditDescriptionFormFields -> Cmd Msg +updateProjectDescription appContext projectRef fields = + ShareApi.updateProject projectRef + (ShareApi.ProjectDescriptionUpdate (editDescriptionFieldsToDescriptionUpdate fields)) + |> HttpApi.toRequestWithEmptyResponse SaveDescriptionFinished + |> HttpApi.perform appContext.api + + + +-- SUBSCRIPTIONS + + +subscriptions : Sub Msg +subscriptions = + KeyboardEvent.subscribe KeyboardEvent.Keydown Keydown + + + +-- VIEW + + +viewLoadingPage : ProjectRef -> PageLayout msg +viewLoadingPage projectRef = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + listing = + ProjectListing.projectListing + { ref = projectRef + , visibility = Project.Public + } + |> ProjectListing.huge + |> ProjectListing.subdued + |> ProjectListing.view + + content = + PageContent.oneColumn + [ ReadmeCard.viewLoading ] + |> PageContent.withPageTitle (PageTitle.custom [ listing ] |> PageTitle.withRightSide [ shape Placeholder.Large ]) + |> PageContent.withLeftAside + [ div [ class "project-overview-page_sidebar_loading" ] + [ shape Placeholder.Small + , shape Placeholder.Tiny + , shape Placeholder.Medium + ] + ] + in + PageLayout.centeredLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewReadmeInstructionsModal : ProjectRef -> Html Msg +viewReadmeInstructionsModal projectRef = + let + step1 = + Steps.step "Write the README" + [ p [] [ text "Define a Unison term named README and add it to your codebase in the root of your Project." ] + ] + + pushCommand = + "push " ++ ProjectRef.toString projectRef + + pullCommand = + "pull " ++ ProjectRef.toString projectRef + + step2 = + Steps.step "Push the README to Unison Share" + [ p [] + [ text "Push your new README to your Project on Unison Share." ] + , CopyField.copyField (\_ -> NoOp) pushCommand |> CopyField.withPrefix ".>" |> CopyField.view + , p [ class "pull-hint" ] + [ Icon.view Icon.bulb + , span [] + [ text "If you have code on Unison Share already, pulling with " + , span [ class "inline-code" ] [ text pullCommand ] + , text " might be needed first." + ] + ] + ] + + steps = + Steps.steps step1 [ step2 ] + + content = + Modal.Content + (div + [] + [ p [] [ text "A Project README is an excellent way to provide in-depth status, details, and documentation for your Project." ] + , UI.divider + , Steps.view steps + , footer [ class "modal-actions" ] + [ Button.iconThenLabel CloseModal Icon.thumbsUp "Got it!" + |> Button.emphasized + |> Button.medium + |> Button.view + ] + ] + ) + in + Modal.modal "project-readme-instructions-modal" CloseModal content + |> Modal.withHeader "Customize your Project with a README" + |> Modal.view + + +viewEditDescriptionModal : EditDescriptionForm -> Html Msg +viewEditDescriptionModal descriptionForm = + let + form_ f = + form [ class "description-form" ] + [ p [] [ text "Describe your project with a brief summary and topic tags." ] + , TextField.field UpdateSummaryField "Summary" f.summary + |> TextField.withHelpText (String.fromInt (unicodeStringLength f.summary) ++ "/100 characters.") + |> TextField.withRows 2 + |> TextField.withMaxlength 100 + |> TextField.withAutofocus + |> TextField.view + + {--, TextField.field UpdateTagsField "Topic tags" f.tags + |> TextField.withHelpText "Separate with spaces." + |> TextField.view + --} + ] + + buttons_ = + { cancel = + Button.button CloseModal "Cancel" + |> Button.subdued + |> Button.medium + , save = + Button.button SaveDescription "Save" + |> Button.emphasized + |> Button.medium + } + + disabledButtons = + { cancel = Button.disabled buttons_.cancel, save = Button.disabled buttons_.save } + + content_ buttons stateClass message form__ = + div + [ class "edit-project-description-modal_content", class stateClass ] + [ form__ + , footer [ class "actions" ] + [ message + , div [ class "buttons" ] + [ buttons.cancel |> Button.view + , buttons.save |> Button.view + ] + ] + ] + + content = + case descriptionForm of + Editing f -> + content_ buttons_ "editing" UI.nothing (form_ f) + + Saving f -> + content_ disabledButtons "saving" (StatusBanner.working "Saving..") (form_ f) + + SaveSuccessful -> + div [ class "save-success" ] + [ StatusIndicator.good + |> StatusIndicator.large + |> StatusIndicator.view + ] + + SaveFailed f _ -> + content_ buttons_ "save-failed" (StatusBanner.bad "Save failed, try again") (form_ f) + in + Modal.modal "edit-project-description-modal" CloseModal (Modal.Content content) + |> Modal.withHeader "Edit Project Description" + |> Modal.view + + +viewReadmeEmptyState : Session -> ProjectDetails -> Html Msg +viewReadmeEmptyState session project = + let + hasProjectAccess = + Session.hasProjectAccess project.ref session + + cta = + if hasProjectAccess then + Button.iconThenLabel + ShowReadmeInstructionsModal + Icon.graduationCap + "Learn how to add a Project README" + |> Button.decorativeBlue + + else + Button.iconThenLabel_ + (Link.projectBranchRoot project.ref (Project.defaultBrowsingBranch project)) + Icon.ability + "Browse Project Code" + in + EmptyState.iconCloud + (EmptyState.CustomCenterPiece + (div + [ class "project-overview-page_empty-state_center-piece" ] + [ ProjectRef.viewHashvatar project.ref ] + ) + ) + |> EmptyState.withContent [ ProjectRef.view project.ref, cta |> Button.medium |> Button.view ] + |> EmptyStateCard.view + + +viewReadmeCard : Session -> ProjectDetails -> WebData ( Maybe Readme, ReadmeCard.Model ) -> Html Msg +viewReadmeCard session project readme = + case readme of + NotAsked -> + ReadmeCard.viewLoading + + Loading -> + ReadmeCard.viewLoading + + Success ( Just rm, card ) -> + Html.map ReadmeCardMsg (ReadmeCard.asCard card rm |> Card.asContained |> Card.view) + + Success ( Nothing, _ ) -> + viewReadmeEmptyState session project + + Failure (Http.BadStatus 404) -> + viewReadmeEmptyState session project + + Failure _ -> + ReadmeCard.viewError RetryFetchReadme + + +viewDependencies : List ProjectDependency -> Html msg +viewDependencies deps = + div [ class "project-dependencies" ] [ strong [] [ text "Dependencies" ], deps |> List.map ProjectDependency.toTag |> Tag.viewTags ] + + +view_ : Session -> ProjectDetails -> Model -> ( Maybe (List (Html Msg)), PageContent Msg ) +view_ session project model = + let + viewKpi icon num labelSingular labelPlural kpiDescription = + KpiTag.kpiTag labelSingular num + |> KpiTag.withPlural labelPlural + |> KpiTag.withIcon icon + |> KpiTag.withTooltip + (Tooltip.text kpiDescription + |> Tooltip.tooltip + |> Tooltip.withArrow Tooltip.Middle + |> Tooltip.withPosition Tooltip.LeftOf + ) + |> KpiTag.view + + favKpi icon = + viewKpi + icon + project.numFavs + "Fav" + "Faves" + "Total number of favorites" + + fav = + case ( session, project.isFaved ) of + ( Session.SignedIn _, Project.NotFaved ) -> + Click.view [ class "not-faved" ] + [ favKpi Icon.heartOutline ] + (Click.onClick ToggleProjectFav) + + ( Session.SignedIn _, Project.Faved ) -> + Click.view [ class "is-faved" ] + [ favKpi Icon.heart ] + (Click.onClick ToggleProjectFav) + + ( Session.SignedIn _, Project.JustFaved ) -> + Click.view [ class "is-faved just-faved" ] + [ favKpi Icon.heart ] + (Click.onClick ToggleProjectFav) + + _ -> + favKpi Icon.heartOutline + + rightSide = + [ div [ class "kpis" ] + [ fav ] + + {- Turn off release downloads for now + , viewKpi + Icon.download + (Project.fourWeekTotalDownloads project) + "Download" + "Downloads" + "Total downloads for the last 4 weeks" + ] + -} + , Button.iconThenLabel ShowUseProjectModal Icon.download "Use Project" + |> Button.medium + |> Button.positive + |> Button.view + ] + + pageTitle = + PageTitle.custom + [ ProjectListing.projectListing project + |> ProjectListing.huge + |> ProjectListing.withClick Link.userProfile Link.projectOverview + |> ProjectListing.view + ] + |> PageTitle.withRightSide rightSide + + hasProjectAccess = + Session.hasProjectAccess project.ref session + + showIfAccess c = + if hasProjectAccess then + Just c + + else + Nothing + + edit icon label = + Button.iconThenLabel ShowEditDescriptionModal icon label + |> Button.small + |> Button.outlined + |> showIfAccess + + summaryAndTags = + case ( project.summary, Set.toList project.tags ) of + ( Just s, [] ) -> + Just + [ div [ class "project-description" ] + [ div [ class "project-summary" ] [ text s ] + , edit Icon.writingPad "Edit summary" + |> Maybe.map Button.view + |> Maybe.withDefault UI.nothing + ] + ] + + ( Just s, tags ) -> + Just + [ div [ class "project-description" ] + [ div [ class "project-summary" ] [ text s ] + , tags |> List.map Tag.tag |> Tag.viewTags + , Button.icon ShowEditDescriptionModal Icon.writingPad + |> Button.small + |> Button.outlined + |> showIfAccess + |> MaybeE.unwrap UI.nothing Button.view + ] + ] + + ( Nothing, [] ) -> + if hasProjectAccess then + Just + [ div [ class "project-description-empty-state" ] + [ text "Add a bit of detail with a summary." + , edit Icon.writingPad "Add now " + |> MaybeE.unwrap UI.nothing Button.view + ] + ] + + else + Nothing + + ( Nothing, tags ) -> + if hasProjectAccess then + Just + [ div [] + [ div + [ class "project-description-empty-state" ] + [ text "Add a bit of detail with a summary." + , edit Icon.writingPad "Add now " + |> MaybeE.unwrap UI.nothing Button.view + ] + , tags |> List.map Tag.tag |> Tag.viewTags + ] + ] + + else + Just + [ div [] [ tags |> List.map Tag.tag |> Tag.viewTags ] + ] + + dependencies = + model.dependencies + |> RemoteData.toMaybe + |> Maybe.map viewDependencies + + aside = + case ( summaryAndTags, dependencies ) of + ( Just sumAndTags, Just deps ) -> + Just (sumAndTags ++ [ deps ]) + + ( Nothing, Just deps ) -> + Just [ deps ] + + ( Just sumAndTags, Nothing ) -> + Just sumAndTags + + _ -> + Nothing + + content = + PageContent.oneColumn + [ div + [ class "project-overview-page_layout" ] + [ div + [ class "project-overview-page_content" ] + [ viewReadmeCard session project model.readme ] + ] + ] + |> PageContent.withPageTitle pageTitle + in + ( aside, content ) + + +view : Session -> ProjectRef -> ProjectDetails -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view session projectRef project model = + let + modal = + case model.modal of + NoModal -> + Nothing + + ReadmeInstructionsModal -> + Just (viewReadmeInstructionsModal projectRef) + + EditDescriptionModal form -> + Just (viewEditDescriptionModal form) + + FinderModal fm -> + Just (Html.map FinderMsg (Finder.view fm)) + + ( aside, content ) = + view_ session project model + in + case aside of + Just aside_ -> + ( PageLayout.centeredLayout (PageContent.withLeftAside aside_ content) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Nothing -> + ( PageLayout.centeredNarrowLayout content + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) diff --git a/src/UnisonShare/Page/ProjectPage.elm b/src/UnisonShare/Page/ProjectPage.elm new file mode 100644 index 00000000..3999e8f4 --- /dev/null +++ b/src/UnisonShare/Page/ProjectPage.elm @@ -0,0 +1,1579 @@ +module UnisonShare.Page.ProjectPage exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.Perspective as Perspective +import Code.ProjectSlug as ProjectSlug +import Code.Version as Version exposing (Version) +import Html exposing (Html, br, div, footer, form, h1, h3, p, span, strong, text) +import Html.Attributes exposing (class) +import Http exposing (Error(..)) +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.AnchoredOverlay as AnchoredOverlay +import UI.Button as Button +import UI.Card as Card +import UI.Click as Click +import UI.CopyField as CopyField +import UI.Divider as Divider +import UI.EmptyState as EmptyState +import UI.EmptyStateCard as EmptyStateCard +import UI.ErrorCard as ErrorCard +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.Modal as Modal +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.Sidebar as Sidebar +import UI.StatusBanner as StatusBanner +import UI.StatusIndicator as StatusIndicator +import UI.ViewMode as ViewMode exposing (ViewMode) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.CodeBrowsingContext as CodeBrowsingContext +import UnisonShare.CodebaseStatus as CodebaseStatus +import UnisonShare.Contribution.ContributionRef as ContributionRef exposing (ContributionRef) +import UnisonShare.Page.CodePage as CodePage +import UnisonShare.Page.ProjectBranchesPage as ProjectBranchesPage +import UnisonShare.Page.ProjectContributionPage as ProjectContributionPage +import UnisonShare.Page.ProjectContributionsPage as ProjectContributionsPage +import UnisonShare.Page.ProjectOverviewPage as ProjectOverviewPage +import UnisonShare.Page.ProjectPageHeader as ProjectPageHeader +import UnisonShare.Page.ProjectReleasePage as ProjectReleasePage +import UnisonShare.Page.ProjectReleasesPage as ProjectReleasesPage +import UnisonShare.Page.ProjectSettingsPage as ProjectSettingsPage +import UnisonShare.Page.ProjectTicketPage as ProjectTicketPage +import UnisonShare.Page.ProjectTicketsPage as ProjectTicketsPage +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project as Project exposing (ProjectDetails) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Route as Route exposing (CodeRoute, ProjectRoute(..)) +import UnisonShare.Session as Session exposing (Session) +import UnisonShare.SwitchBranch as SwitchBranch +import UnisonShare.Ticket.TicketRef as TicketRef exposing (TicketRef) + + + +-- MODEL + + +type SubPage + = Overview ProjectOverviewPage.Model + | Branches ProjectBranchesPage.Model + | Code BranchRef ViewMode CodePage.Model + | Release Version ProjectReleasePage.Model + | Releases ProjectReleasesPage.Model + | Contribution ContributionRef ProjectContributionPage.Model + | Contributions ProjectContributionsPage.Model + | Ticket TicketRef ProjectTicketPage.Model + | Tickets ProjectTicketsPage.Model + | Settings ProjectSettingsPage.Model + + +type alias DeleteProject = + { confirmText : String, deleting : WebData () } + + +type ProjectPageModal + = NoModal + | UseProjectModal + | DeleteProjectModal DeleteProject + + +type alias Model = + { subPage : SubPage + , project : WebData ProjectDetails + , modal : ProjectPageModal + , switchBranch : SwitchBranch.Model + , mobileNavIsOpen : Bool + } + + +init : AppContext -> ProjectRef -> ProjectRoute -> ( Model, Cmd Msg ) +init appContext projectRef route = + let + ( subPage, pageCmd ) = + case route of + ProjectOverview -> + let + ( overviewPage, overviewCmd ) = + ProjectOverviewPage.init appContext projectRef + in + ( Overview overviewPage, Cmd.map ProjectOverviewPageMsg overviewCmd ) + + ProjectBranches -> + let + ( branchesPage, branchesCmd ) = + ProjectBranchesPage.init appContext projectRef + in + ( Branches branchesPage, Cmd.map ProjectBranchesPageMsg branchesCmd ) + + ProjectBranch branchRef vm codeRoute -> + let + codeBrowsingContext = + CodeBrowsingContext.project projectRef branchRef + + ( codePage, codePageCmd ) = + CodePage.init appContext codeBrowsingContext codeRoute + in + ( Code branchRef vm codePage, Cmd.map CodePageMsg codePageCmd ) + + ProjectContribution contribRef contribRoute -> + let + ( contribution_, contribCmd ) = + ProjectContributionPage.init + appContext + projectRef + contribRef + contribRoute + in + ( Contribution contribRef contribution_, Cmd.map ProjectContributionPageMsg contribCmd ) + + ProjectContributions -> + let + ( contributions_, contribsCmd ) = + ProjectContributionsPage.init + appContext + projectRef + in + ( Contributions contributions_, Cmd.map ProjectContributionsPageMsg contribsCmd ) + + ProjectTicket ticketRef -> + let + ( ticket_, ticketCmd ) = + ProjectTicketPage.init + appContext + projectRef + ticketRef + in + ( Ticket ticketRef ticket_, Cmd.map ProjectTicketPageMsg ticketCmd ) + + ProjectTickets -> + let + ( tickets_, ticketsCmd ) = + ProjectTicketsPage.init appContext projectRef + in + ( Tickets tickets_, Cmd.map ProjectTicketsPageMsg ticketsCmd ) + + ProjectRelease version -> + let + ( release_, releaseCmd ) = + ProjectReleasePage.init + appContext + projectRef + version + in + ( Release version release_, Cmd.map ProjectReleasePageMsg releaseCmd ) + + ProjectReleases -> + let + ( releases_, releasesCmd ) = + ProjectReleasesPage.init + appContext + projectRef + Loading + in + ( Releases releases_, Cmd.map ProjectReleasesPageMsg releasesCmd ) + + ProjectSettings -> + ( Settings ProjectSettingsPage.init, Cmd.none ) + in + ( { subPage = subPage + , project = Loading + , modal = NoModal + , switchBranch = SwitchBranch.init + , mobileNavIsOpen = False + } + , Cmd.batch [ fetchProject appContext projectRef, pageCmd ] + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchProjectFinished (WebData ProjectDetails) + | ToggleProjectFav + | SetProjectFavFinished Bool (HttpResult ()) + | ShowUseProjectModal + | ShowDeleteProjectModal + | YesDeleteProject + | UpdateDeleteProjectModalConfirmText String + | DeleteProjectFinished (HttpResult ()) + | CloseModal + | ToggleMobileNav + | SwitchBranchMsg SwitchBranch.Msg + | ProjectOverviewPageMsg ProjectOverviewPage.Msg + | CodePageMsg CodePage.Msg + | ProjectReleasePageMsg ProjectReleasePage.Msg + | ProjectReleasesPageMsg ProjectReleasesPage.Msg + | ProjectContributionPageMsg ProjectContributionPage.Msg + | ProjectContributionsPageMsg ProjectContributionsPage.Msg + | ProjectTicketPageMsg ProjectTicketPage.Msg + | ProjectTicketsPageMsg ProjectTicketsPage.Msg + | ProjectBranchesPageMsg ProjectBranchesPage.Msg + | ProjectSettingsPageMsg ProjectSettingsPage.Msg + | ChangeRouteTo Route.Route + + +update : AppContext -> ProjectRef -> ProjectRoute -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef route msg model = + let + codeBrowsingContext bs = + CodeBrowsingContext.project projectRef bs + in + case ( model.subPage, msg ) of + ( _, ChangeRouteTo r ) -> + ( model, Route.navigate appContext.navKey r ) + + ( _, FetchProjectFinished project ) -> + let + modelWithProject = + { model | project = project } + in + case ( model.subPage, project ) of + ( Releases rm, Success project_ ) -> + case project_.latestVersion of + Just v -> + let + ( releases, cmd ) = + ProjectReleasesPage.fetchLatestReleaseNotesAndUpdate + appContext + projectRef + rm + v + in + ( { modelWithProject | subPage = Releases releases } + , Cmd.map ProjectReleasesPageMsg cmd + ) + + Nothing -> + ( modelWithProject, Cmd.none ) + + ( Releases rm, _ ) -> + let + releases = + ProjectReleasesPage.updateWithNoLatestReleaseNotes rm + in + ( { modelWithProject | subPage = Releases releases } + , Cmd.none + ) + + ( Overview ov, Success project_ ) -> + let + ( overview, cmd ) = + ProjectOverviewPage.fetchDependenciesAndUpdate + appContext + project_ + ov + in + ( { modelWithProject | subPage = Overview overview } + , Cmd.map ProjectOverviewPageMsg cmd + ) + + _ -> + ( modelWithProject, Cmd.none ) + + ( _, ToggleProjectFav ) -> + case model.project of + Success project -> + let + project_ = + Project.toggleFav project + in + ( { model | project = Success project_ }, setProjectFav appContext project_ ) + + _ -> + ( model, Cmd.none ) + + ( _, SetProjectFavFinished _ isFavedResult ) -> + case ( model.project, isFavedResult ) of + -- We have a project, but the API request to faving failed + ( Success project, Err _ ) -> + -- Reverting the fav change by re-toggling Project.isFaved. + ( { model | modal = NoModal, project = Success (Project.toggleFav project) }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + ( _, ToggleMobileNav ) -> + ( { model | mobileNavIsOpen = not model.mobileNavIsOpen }, Cmd.none ) + + ( _, ShowDeleteProjectModal ) -> + ( { model | modal = DeleteProjectModal { confirmText = "", deleting = NotAsked } }, Cmd.none ) + + ( _, YesDeleteProject ) -> + case model.modal of + DeleteProjectModal del -> + if del.confirmText == "delete" then + ( { model | modal = DeleteProjectModal { del | deleting = Loading } } + , deleteProject appContext projectRef + ) + + else + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + ( _, DeleteProjectFinished res ) -> + case model.modal of + DeleteProjectModal del -> + let + deleting = + RemoteData.fromResult res + + modal = + DeleteProjectModal { del | deleting = deleting } + + cmd = + case res of + Ok _ -> + Util.delayMsg 1500 (ChangeRouteTo Route.catalog) + + _ -> + Cmd.none + in + ( { model | modal = modal }, cmd ) + + _ -> + ( model, Cmd.none ) + + ( _, UpdateDeleteProjectModalConfirmText t ) -> + case model.modal of + DeleteProjectModal del -> + ( { model | modal = DeleteProjectModal { del | confirmText = t } }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + -- Sub msgs + ( Overview overviewPage, ProjectOverviewPageMsg overviewPageMsg ) -> + case route of + Route.ProjectOverview -> + let + ( overviewPage_, overviewCmd, out ) = + ProjectOverviewPage.update appContext + projectRef + model.project + overviewPageMsg + overviewPage + + ( model_, cmd ) = + case ( model.project, out ) of + ( Success _, ProjectOverviewPage.RequestToShowUseProjectModal ) -> + ( { model | modal = UseProjectModal }, Cmd.none ) + + ( Success project, ProjectOverviewPage.RequestToToggleProjectFav ) -> + let + project_ = + Project.toggleFav project + in + ( { model | modal = NoModal, project = Success project_ }, setProjectFav appContext project_ ) + + ( Success project, ProjectOverviewPage.ProjectDescriptionUpdated description ) -> + let + updatedProject = + { project + | summary = description.summary + , tags = description.tags + } + in + ( { model | modal = NoModal, project = Success updatedProject }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + in + ( { model_ | subPage = Overview overviewPage_ } + , Cmd.batch [ Cmd.map ProjectOverviewPageMsg overviewCmd, cmd ] + ) + + _ -> + ( model, Cmd.none ) + + ( Branches branchesPage, ProjectBranchesPageMsg branchesMsg ) -> + case route of + Route.ProjectBranches -> + let + ( branchesPage_, branchesCmd ) = + ProjectBranchesPage.update appContext projectRef branchesMsg branchesPage + in + ( { model | subPage = Branches branchesPage_ } + , Cmd.map ProjectBranchesPageMsg branchesCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Code branchRef viewMode codePage, CodePageMsg codePageMsg ) -> + case route of + Route.ProjectBranch _ _ cr -> + let + ( codePage_, codePageCmd ) = + CodePage.update appContext (codeBrowsingContext branchRef) viewMode cr codePageMsg codePage + in + ( { model | subPage = Code branchRef viewMode codePage_ } + , Cmd.map CodePageMsg codePageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Release currentVersion releasePage, ProjectReleasePageMsg releasePageMsg ) -> + case route of + Route.ProjectRelease version -> + if Version.equals currentVersion version then + let + ( releasePage_, releasePageCmd ) = + ProjectReleasePage.update appContext projectRef releasePageMsg releasePage + in + ( { model | subPage = Release version releasePage_ } + , Cmd.map ProjectReleasePageMsg releasePageCmd + ) + + else + let + ( releasePage_, releasePageCmd ) = + ProjectReleasePage.init appContext projectRef version + in + ( { model | subPage = Release version releasePage_ } + , Cmd.map ProjectReleasePageMsg releasePageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Releases releasesPage, ProjectReleasesPageMsg releasesPageMsg ) -> + case route of + Route.ProjectReleases -> + let + ( releasesPage_, releasesPageCmd, out ) = + ProjectReleasesPage.update appContext projectRef releasesPageMsg releasesPage + + project = + case out of + ProjectReleasesPage.None -> + model.project + + ProjectReleasesPage.PublishedNewRelease r -> + model.project + |> RemoteData.map (\p -> { p | latestVersion = Just r.version }) + in + ( { model | subPage = Releases releasesPage_, project = project } + , Cmd.map ProjectReleasesPageMsg releasesPageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Contribution currentContribRef contribPage, ProjectContributionPageMsg contribMsg ) -> + case route of + Route.ProjectContribution contribRef contribRoute -> + if ContributionRef.equals currentContribRef contribRef then + let + ( contribPage_, contribPageCmd ) = + ProjectContributionPage.update appContext + projectRef + contribRef + contribRoute + contribMsg + contribPage + in + ( { model | subPage = Contribution contribRef contribPage_ } + , Cmd.map ProjectContributionPageMsg contribPageCmd + ) + + else + let + ( contribPage_, contribPageCmd ) = + ProjectContributionPage.init appContext projectRef contribRef contribRoute + in + ( { model | subPage = Contribution contribRef contribPage_ } + , Cmd.map ProjectContributionPageMsg contribPageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Contributions contribsPage, ProjectContributionsPageMsg contribsMsg ) -> + case route of + Route.ProjectContributions -> + let + ( contribsPage_, contribsPageCmd ) = + ProjectContributionsPage.update appContext projectRef contribsMsg contribsPage + in + ( { model | subPage = Contributions contribsPage_ } + , Cmd.map ProjectContributionsPageMsg contribsPageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Ticket currentTicketRef ticketPage, ProjectTicketPageMsg ticketMsg ) -> + case route of + Route.ProjectTicket ticketRef -> + if TicketRef.equals currentTicketRef ticketRef then + let + ( ticketPage_, ticketPageCmd ) = + ProjectTicketPage.update appContext + projectRef + ticketRef + ticketMsg + ticketPage + in + ( { model | subPage = Ticket ticketRef ticketPage_ } + , Cmd.map ProjectTicketPageMsg ticketPageCmd + ) + + else + let + ( ticketPage_, ticketPageCmd ) = + ProjectTicketPage.init appContext projectRef ticketRef + in + ( { model | subPage = Ticket ticketRef ticketPage_ } + , Cmd.map ProjectTicketPageMsg ticketPageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Tickets ticketsPage, ProjectTicketsPageMsg ticketsMsg ) -> + case route of + Route.ProjectTickets -> + let + ( ticketsPage_, ticketsPageCmd ) = + ProjectTicketsPage.update appContext projectRef ticketsMsg ticketsPage + in + ( { model | subPage = Tickets ticketsPage_ } + , Cmd.map ProjectTicketsPageMsg ticketsPageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Settings settings, ProjectSettingsPageMsg settingsPageMsg ) -> + case ( model.project, route ) of + ( Success project, Route.ProjectSettings ) -> + let + ( settings_, settingsPageCmd, out ) = + ProjectSettingsPage.update appContext project settingsPageMsg settings + + ( project_, modal ) = + case out of + ProjectSettingsPage.ProjectUpdated p -> + ( p, model.modal ) + + ProjectSettingsPage.ShowDeleteProjectModalRequest -> + ( project, DeleteProjectModal { confirmText = "", deleting = NotAsked } ) + + _ -> + ( project, model.modal ) + in + ( { model + | project = Success project_ + , subPage = Settings settings_ + , modal = modal + } + , Cmd.map ProjectSettingsPageMsg settingsPageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( _, SwitchBranchMsg sbMsg ) -> + let + ( switchBranch, switchBranchCmd, out ) = + SwitchBranch.update appContext projectRef sbMsg model.switchBranch + + navCmd = + case out of + SwitchBranch.SwitchToBranchRequest branchRef -> + Route.navigate + appContext.navKey + (Route.projectBranchRoot + projectRef + branchRef + Perspective.relativeRootPerspective + ) + + _ -> + Cmd.none + in + ( { model | switchBranch = switchBranch }, Cmd.batch [ Cmd.map SwitchBranchMsg switchBranchCmd, navCmd ] ) + + ( _, ShowUseProjectModal ) -> + ( { model | modal = UseProjectModal }, Cmd.none ) + + ( _, CloseModal ) -> + ( { model | modal = NoModal }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + +{-| Pass through to CodePage. Used by App when routes change +-} +updateSubPage : AppContext -> ProjectRef -> Model -> ProjectRoute -> ( Model, Cmd Msg ) +updateSubPage appContext projectRef model route = + let + codeBrowsingContext br = + CodeBrowsingContext.project projectRef br + + newCodePage branchRef vm codeRoute = + let + ( codePage, codePageCmd ) = + CodePage.init appContext (codeBrowsingContext branchRef) codeRoute + in + ( { model | subPage = Code branchRef vm codePage } + , Cmd.map CodePageMsg codePageCmd + ) + in + case route of + ProjectOverview -> + case model.subPage of + Overview _ -> + ( model, Cmd.none ) + + _ -> + let + ( overviewPage, overviewCmd ) = + ProjectOverviewPage.init appContext projectRef + in + ( { model | subPage = Overview overviewPage }, Cmd.map ProjectOverviewPageMsg overviewCmd ) + + ProjectBranches -> + case model.subPage of + Branches _ -> + ( model, Cmd.none ) + + _ -> + let + ( branchesPage, branchesCmd ) = + ProjectBranchesPage.init appContext projectRef + in + ( { model | subPage = Branches branchesPage }, Cmd.map ProjectBranchesPageMsg branchesCmd ) + + ProjectBranch branchRef vm codeRoute -> + case model.subPage of + Code oldBranchRef _ codeSubPage -> + if BranchRef.equals oldBranchRef branchRef then + let + ( codePage, codePageCmd ) = + CodePage.updateSubPage appContext (codeBrowsingContext branchRef) codeRoute codeSubPage + in + ( { model | subPage = Code branchRef vm codePage } + , Cmd.map CodePageMsg codePageCmd + ) + + else + newCodePage branchRef vm codeRoute + + _ -> + newCodePage branchRef vm codeRoute + + ProjectContribution contribRef contribRoute -> + let + newContribPage = + let + ( contrib_, contribCmd ) = + ProjectContributionPage.init appContext projectRef contribRef contribRoute + in + ( { model | subPage = Contribution contribRef contrib_ }, Cmd.map ProjectContributionPageMsg contribCmd ) + in + case model.subPage of + Contribution cRef page -> + if ContributionRef.equals cRef contribRef then + let + ( contrib_, contribCmd ) = + ProjectContributionPage.updateSubPage appContext projectRef contribRef contribRoute page + in + ( { model | subPage = Contribution contribRef contrib_ }, Cmd.map ProjectContributionPageMsg contribCmd ) + + else + newContribPage + + _ -> + newContribPage + + ProjectContributions -> + case model.subPage of + Contributions _ -> + ( model, Cmd.none ) + + _ -> + let + ( contribs_, contribsCmd ) = + ProjectContributionsPage.init + appContext + projectRef + in + ( { model | subPage = Contributions contribs_ }, Cmd.map ProjectContributionsPageMsg contribsCmd ) + + ProjectTicket ticketRef -> + case model.subPage of + Ticket _ _ -> + ( model, Cmd.none ) + + _ -> + let + ( ticket_, ticketCmd ) = + ProjectTicketPage.init appContext projectRef ticketRef + in + ( { model | subPage = Ticket ticketRef ticket_ }, Cmd.map ProjectTicketPageMsg ticketCmd ) + + ProjectTickets -> + case model.subPage of + Tickets _ -> + ( model, Cmd.none ) + + _ -> + let + ( tickets_, ticketsCmd ) = + ProjectTicketsPage.init appContext projectRef + in + ( { model | subPage = Tickets tickets_ }, Cmd.map ProjectTicketsPageMsg ticketsCmd ) + + ProjectReleases -> + case model.subPage of + Releases _ -> + ( model, Cmd.none ) + + _ -> + let + ( releasesPage_, releasesPageCmd ) = + ProjectReleasesPage.init + appContext + projectRef + (RemoteData.map .latestVersion model.project) + in + ( { model | subPage = Releases releasesPage_ }, Cmd.map ProjectReleasesPageMsg releasesPageCmd ) + + ProjectRelease version -> + case model.subPage of + Release _ _ -> + ( model, Cmd.none ) + + _ -> + let + ( releasePage_, releasePageCmd ) = + ProjectReleasePage.init appContext projectRef version + in + ( { model | subPage = Release version releasePage_ }, Cmd.map ProjectReleasePageMsg releasePageCmd ) + + ProjectSettings -> + case model.subPage of + Settings _ -> + ( model, Cmd.none ) + + _ -> + ( { model | subPage = Settings ProjectSettingsPage.init }, Cmd.none ) + + + +-- EFFECTS + + +fetchProject : AppContext -> ProjectRef -> Cmd Msg +fetchProject appContext projectRef = + ShareApi.project projectRef + |> HttpApi.toRequest Project.decodeDetails (RemoteData.fromResult >> FetchProjectFinished) + |> HttpApi.perform appContext.api + + +setProjectFav : AppContext -> ProjectDetails -> Cmd Msg +setProjectFav appContext project = + let + isFaved = + Project.isFavedToBool project.isFaved + in + ShareApi.updateProjectFav project.ref isFaved + |> HttpApi.toRequestWithEmptyResponse (SetProjectFavFinished isFaved) + |> HttpApi.perform appContext.api + + +deleteProject : AppContext -> ProjectRef -> Cmd Msg +deleteProject appContext projectRef = + ShareApi.deleteProject projectRef + |> HttpApi.toRequestWithEmptyResponse DeleteProjectFinished + |> HttpApi.perform appContext.api + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions model = + case model.subPage of + Overview _ -> + Sub.map ProjectOverviewPageMsg ProjectOverviewPage.subscriptions + + Code _ _ ucp -> + Sub.map CodePageMsg (CodePage.subscriptions ucp) + + _ -> + Sub.none + + + +-- EFFECTS + + +navigateToCode : AppContext -> ProjectRef -> BranchRef -> CodeRoute -> Cmd Msg +navigateToCode appContext projectRef branchRef codeRoute = + Route.navigate appContext.navKey (Route.projectBranch projectRef branchRef codeRoute) + + + +-- VIEW + + +viewDeleteProjectModal : ProjectRef -> DeleteProject -> Html Msg +viewDeleteProjectModal projectRef { confirmText, deleting } = + let + projectRef_ = + ProjectRef.toString projectRef + + ( statusBanner, overlay ) = + case deleting of + NotAsked -> + ( UI.nothing, UI.nothing ) + + Loading -> + ( StatusBanner.working "Deleting..", div [ class "delete-project-modal_overlay-deleting" ] [] ) + + Success _ -> + ( UI.nothing + , div + [ class "delete-project-modal_overlay-success" + ] + [ StatusIndicator.good |> StatusIndicator.large |> StatusIndicator.view + , div [] + [ strong [] [ text projectRef_ ] + , br [] [] + , text " successfully deleted" + ] + ] + ) + + Failure _ -> + ( StatusBanner.bad "Delete project failed", UI.nothing ) + + content = + div [ class "delete-project-modal_content" ] + [ p [] + [ text "You're about to permanently delete " + , strong [] [ text projectRef_ ] + , text "." + ] + , StatusBanner.bad "Take care—project deletions can't be undone!" + , Divider.divider + |> Divider.small + |> Divider.withoutMargin + |> Divider.view + , form [ class "description-form" ] + [ TextField.field UpdateDeleteProjectModalConfirmText "Type \"delete\" to confirm." confirmText + |> TextField.withAutofocus + |> TextField.view + ] + , footer + [ class "delete-project-modal_actions" ] + [ statusBanner + , Button.button CloseModal "Cancel" + |> Button.subdued + |> Button.medium + |> Button.view + , Button.button YesDeleteProject "Yes, delete project" + |> Button.critical + |> Button.medium + |> Button.view + ] + , overlay + ] + in + Modal.modal "delete-project-modal" CloseModal (Modal.Content content) + |> Modal.withHeader "Permanently Delete Project?" + |> Modal.view + + +viewUseProjectModal : ProjectDetails -> Maybe BranchRef -> Html Msg +viewUseProjectModal project branchRef = + let + projectRef_ = + ProjectRef.toString project.ref + + libVersion v = + "lib." + ++ ProjectSlug.toNamespaceString (ProjectRef.slug project.ref) + ++ "_" + ++ Version.toNamespaceString v + + pullCommand_ br = + case br of + BranchRef.ReleaseBranchRef v -> + "pull " + ++ projectRef_ + ++ "/releases/" + ++ Version.toString v + ++ " " + ++ libVersion v + + _ -> + "pull " + ++ projectRef_ + ++ "/" + ++ BranchRef.toString br + ++ " lib." + ++ ProjectSlug.toNamespaceString (ProjectRef.slug project.ref) + + pullHint_ source = + [ text "UCM will clone the ", strong [] [ text source ], text " into the lib namespace." ] + + upgradeHint_ v = + div [ class "upgrade-hint" ] + [ div [ class "upgrade-icon" ] [ Icon.view Icon.arrowUp ] + , div [ class "upgrade-hint_content" ] + [ div [] [ text "Upgrading from a previous version? Pull using the above and then run:" ] + , div [ class "monospace" ] [ text ("myProject/main> upgrade " ++ libVersion v) ] + ] + ] + + { activeBranchRef, modalTitle, pullCommand, pullHint, upgradeHint } = + case ( branchRef, project.latestVersion, project.defaultBranch ) of + ( Just b, _, _ ) -> + case b of + BranchRef.ReleaseBranchRef v -> + { activeBranchRef = b + , modalTitle = "/" ++ BranchRef.toString b + , pullCommand = pullCommand_ b + , pullHint = pullHint_ (BranchRef.toString b ++ " release") + , upgradeHint = Just (upgradeHint_ v) + } + + _ -> + { activeBranchRef = b + , modalTitle = "/" ++ BranchRef.toString b + , pullCommand = pullCommand_ b + , pullHint = pullHint_ (BranchRef.toString b ++ " branch") + , upgradeHint = Nothing + } + + ( Nothing, Just v, _ ) -> + { activeBranchRef = BranchRef.ReleaseBranchRef v + , modalTitle = "" + , pullCommand = pullCommand_ (BranchRef.releaseBranchRef v) + , pullHint = pullHint_ "latest release" + , upgradeHint = Just (upgradeHint_ v) + } + + ( Nothing, Nothing, Just b ) -> + { activeBranchRef = b + , modalTitle = "/" ++ BranchRef.toString b + , pullCommand = pullCommand_ b + , pullHint = pullHint_ (BranchRef.toString b ++ " branch") + , upgradeHint = Nothing + } + + _ -> + { activeBranchRef = BranchRef.main_ + , modalTitle = "/main" + , pullCommand = pullCommand_ BranchRef.main_ + , pullHint = pullHint_ "main branch" + , upgradeHint = Nothing + } + + {- for contribution, people typically want to clone main or a selected branch, not the latest release -} + ( cloneBranch, cloneCommand ) = + case ( branchRef, project.defaultBranch ) of + ( Just b, Just db ) -> + case b of + BranchRef.ReleaseBranchRef _ -> + ( db, "clone " ++ projectRef_ ) + + _ -> + ( b, "clone " ++ projectRef_ ++ "/" ++ BranchRef.toString b ) + + ( Just b, Nothing ) -> + case b of + BranchRef.ReleaseBranchRef _ -> + ( BranchRef.main_, "clone " ++ projectRef_ ) + + _ -> + ( b, "clone " ++ projectRef_ ++ "/" ++ BranchRef.toString b ) + + ( Nothing, Just b ) -> + ( b, "clone " ++ projectRef_ ) + + _ -> + ( BranchRef.main_, "clone " ++ projectRef_ ++ "/" ++ BranchRef.toString BranchRef.main_ ) + + notAReleaseNote = + case activeBranchRef of + BranchRef.ReleaseBranchRef _ -> + UI.nothing + + _ -> + StatusBanner.info_ + (span [] + [ text "Note that " + , strong [] [ text (BranchRef.toString activeBranchRef) ] + , text " is a branch, not the latest release." + ] + ) + + content = + Modal.Content + (div [ class "use-project-modal_content" ] + [ notAReleaseNote + , div [ class "instruction" ] + [ h3 [] [ text "As a dependency" ] + , p [] + [ text "From within your project in UCM, run the " + , strong [] [ text "pull" ] + , text " command:" + ] + , CopyField.copyField (\_ -> NoOp) pullCommand |> CopyField.withPrefix "myProject/main>" |> CopyField.view + , div [ class "hint" ] pullHint + , Maybe.withDefault UI.nothing upgradeHint + ] + , Divider.divider |> Divider.small |> Divider.withoutMargin |> Divider.view + , div [ class "instruction" ] + [ h3 [] [ text "To contribute" ] + , p [] [ text "Run the ", strong [] [ text "clone" ], text " command within UCM:" ] + , CopyField.copyField (\_ -> NoOp) cloneCommand |> CopyField.withPrefix ".>" |> CopyField.view + , div [ class "hint" ] + [ text "UCM will clone the " + , strong [] [ text (BranchRef.toString cloneBranch) ] + , text " branch of this project, and switch you to it." + ] + ] + , div [ class "action" ] [ Button.iconThenLabel CloseModal Icon.thumbsUp "Got it!" |> Button.emphasized |> Button.view ] + ] + ) + in + Modal.modal "use-project-modal" CloseModal content + |> Modal.withHeader ("Use " ++ projectRef_ ++ modalTitle) + |> Modal.view + + +viewErrorPage : AppContext -> SubPage -> ProjectRef -> Error -> AppDocument msg +viewErrorPage appContext subPage projectRef error = + let + pageHeader = + ProjectPageHeader.error projectRef + + projectRef_ = + ProjectRef.toString projectRef + + errorCard = + case error of + BadStatus 404 -> + let + signInSuggest = + case appContext.session of + Session.Anonymous -> + div [ class "project-page_login-tip" ] [ StatusBanner.info "Tip: if you're looking for a private project, try signing in." ] + + _ -> + UI.nothing + in + div [] + [ ErrorCard.errorCard + ("Couldn't find " ++ projectRef_) + ("We looked everywhere, but unfortunately '" ++ projectRef_ ++ "' was nowhere to be found.") + |> ErrorCard.toCard + |> Card.asContainedWithFade + |> Card.view + , signInSuggest + ] + + _ -> + ErrorCard.errorCard + ("Couldn't load " ++ projectRef_) + "Something unexpected happened on our end when loading the Project and we can't display it." + |> ErrorCard.toCard + |> Card.asContainedWithFade + |> Card.view + + page = + case subPage of + Overview _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Branches _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Code _ _ _ -> + PageLayout.sidebarLeftContentLayout + appContext.operatingSystem + (Sidebar.empty "main-sidebar") + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Contribution _ _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Contributions _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Ticket _ _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Tickets _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Release _ _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Releases _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + Settings _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + in + { pageId = "project-page project-page-error" + , title = "Error loading " ++ ProjectRef.toString projectRef + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Just pageHeader + , page = PageLayout.view page + , modal = Nothing + } + + +viewLoadingPage : AppContext -> SubPage -> ProjectRef -> AppDocument Msg +viewLoadingPage appContext subPage projectRef = + let + ( page, pageId ) = + case subPage of + Overview _ -> + ( ProjectOverviewPage.viewLoadingPage projectRef, "project-overview-page" ) + + Branches _ -> + ( ProjectBranchesPage.viewLoadingPage projectRef, "project-branches-page" ) + + Code _ _ _ -> + ( PageLayout.sidebarLeftContentLayout + appContext.operatingSystem + (Sidebar.empty "main-sidebar") + (PageContent.oneColumn [ text "" ]) + PageFooter.pageFooter + , "code-page" + ) + + Contribution _ _ -> + ( PageLayout.map + ProjectContributionPageMsg + ProjectContributionPage.viewLoadingPage + , "project-contribution-page" + ) + + Contributions _ -> + ( PageLayout.map + ProjectContributionsPageMsg + ProjectContributionsPage.viewLoadingPage + , "project-contributions-page" + ) + + Ticket _ _ -> + ( PageLayout.map + ProjectTicketPageMsg + ProjectTicketPage.viewLoadingPage + , "project-ticket-page" + ) + + Tickets _ -> + ( PageLayout.map + ProjectTicketsPageMsg + ProjectTicketsPage.viewLoadingPage + , "project-tickets-page" + ) + + Release version _ -> + ( PageLayout.map + ProjectReleasePageMsg + (ProjectReleasePage.viewLoadingPage version) + , "project-release-page" + ) + + Releases _ -> + ( PageLayout.map + ProjectReleasesPageMsg + (ProjectReleasesPage.viewLoadingPage projectRef) + , "project-releases-page" + ) + + Settings _ -> + ( ProjectSettingsPage.viewLoadingPage, "project-settings-page" ) + in + { pageId = "project-page project-page-loading " ++ pageId + , title = "Loading " ++ ProjectRef.toString projectRef + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Just (ProjectPageHeader.loading projectRef) + , page = PageLayout.view page + , modal = Nothing + } + + +viewProjectEmptyState : Session -> ProjectDetails -> Maybe (Html Msg) -> AppDocument Msg +viewProjectEmptyState session project modal = + let + content = + if Session.isProjectOwner project.ref session then + [ EmptyState.iconCloud (EmptyState.IconCenterPiece Icon.branch) + |> EmptyState.withContent + [ h1 [] [ text "No default branch" ] + , div [ class "empty-state-content" ] + [ p [] + [ text "This happens when you create a project, but haven't pushed a branch called " + , UI.inlineCode [] (text "/main") + , text " yet. " + , br [] [] + , text "Share requires a " + , UI.inlineCode [] (text "/main") + , text " branch to exist for full project functionality." + ] + , p + [ class "delete-project" ] + [ text "Alternatively, you can " + , Button.iconThenLabel ShowDeleteProjectModal Icon.trash "Delete this project" + |> Button.small + |> Button.subdued + |> Button.view + , text " and start over." + ] + ] + ] + |> EmptyStateCard.view + ] + + else + [ text "Nothing to see here yet." ] + + page = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn content) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + in + { pageId = "project-page project-page-empty" + , title = ProjectRef.toString project.ref + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Just (ProjectPageHeader.disabled project.ref) + , page = PageLayout.view page + , modal = modal + } + + +view : AppContext -> ProjectRef -> Model -> AppDocument Msg +view appContext projectRef model = + let + pageHeader activeNavItem branchRef project = + ProjectPageHeader.projectPageHeader + appContext.session + { toggleFavMsg = ToggleProjectFav + , useProjectButtonClickMsg = ShowUseProjectModal + , mobileNavToggleMsg = ToggleMobileNav + , mobileNavIsOpen = model.mobileNavIsOpen + , activeNavItem = activeNavItem + , switchBranch = + AnchoredOverlay.map + SwitchBranchMsg + (SwitchBranch.toAnchoredOverlay projectRef branchRef model.switchBranch) + , contextClick = Click.onClick (ChangeRouteTo (Route.projectOverview projectRef)) + } + project + + title = + ProjectRef.toString projectRef + + appHeader vm = + AppHeader.withViewMode vm (AppHeader.appHeader AppHeader.None) + + modal pageModal = + case ( model.project, model.modal ) of + ( Success p, UseProjectModal ) -> + case model.subPage of + Code br _ _ -> + Just (viewUseProjectModal p (Just br)) + + _ -> + Just (viewUseProjectModal p Nothing) + + ( _, DeleteProjectModal deleteProject_ ) -> + Just (viewDeleteProjectModal projectRef deleteProject_) + + _ -> + pageModal + in + case model.project of + NotAsked -> + viewLoadingPage appContext model.subPage projectRef + + Loading -> + viewLoadingPage appContext model.subPage projectRef + + Failure error -> + viewErrorPage appContext model.subPage projectRef error + + Success project -> + case project.defaultBranch of + Nothing -> + viewProjectEmptyState appContext.session project (modal Nothing) + + Just _ -> + case model.subPage of + Overview overviewPage -> + let + ( overviewPage_, modal_ ) = + ProjectOverviewPage.view + appContext.session + projectRef + project + overviewPage + in + { pageId = "project-page project-overview-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader ProjectPageHeader.Overview + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectOverviewPageMsg overviewPage_) + , modal = modal (Maybe.map (Html.map ProjectOverviewPageMsg) modal_) + } + + Branches branchesPage -> + let + ( branchesPage_, modal_ ) = + ProjectBranchesPage.view + appContext + project + branchesPage + in + { pageId = "project-page project-branches-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader ProjectPageHeader.NoActiveNavItem + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectBranchesPageMsg branchesPage_) + , modal = modal (Maybe.map (Html.map ProjectBranchesPageMsg) modal_) + } + + Code branchRef viewMode_ codePage -> + let + codeBrowsingContext = + CodeBrowsingContext.project projectRef branchRef + + ( codePage_, modal_ ) = + CodePage.view appContext + CodePageMsg + viewMode_ + codeBrowsingContext + CodebaseStatus.NotEmpty + codePage + in + { pageId = "project-page code-page" + , title = title + , appHeader = appHeader viewMode_ + , pageHeader = + Just + (pageHeader ProjectPageHeader.DocsAndCode + branchRef + project + ) + , page = PageLayout.view codePage_ + , modal = modal modal_ + } + + Release version release -> + let + ( release_, modal_ ) = + ProjectReleasePage.view + appContext + project.ref + version + release + in + { pageId = "project-page project-release-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader + ProjectPageHeader.Releases + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectReleasePageMsg release_) + , modal = modal (Maybe.map (Html.map ProjectReleasePageMsg) modal_) + } + + Releases releases -> + let + ( releases_, modal_ ) = + ProjectReleasesPage.view + appContext + project.ref + releases + in + { pageId = "project-page project-releases-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader + ProjectPageHeader.Releases + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectReleasesPageMsg releases_) + , modal = modal (Maybe.map (Html.map ProjectReleasesPageMsg) modal_) + } + + Contribution cRef contribution -> + let + ( contribution_, modal_ ) = + ProjectContributionPage.view + appContext + project.ref + cRef + contribution + in + { pageId = "project-page project-contribution-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader + ProjectPageHeader.Contributions + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectContributionPageMsg contribution_) + , modal = modal (Maybe.map (Html.map ProjectContributionPageMsg) modal_) + } + + Contributions contributions -> + let + ( contributions_, modal_ ) = + ProjectContributionsPage.view + appContext + projectRef + contributions + in + { pageId = "project-page project-contributions-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader + ProjectPageHeader.Contributions + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectContributionsPageMsg contributions_) + , modal = modal (Maybe.map (Html.map ProjectContributionsPageMsg) modal_) + } + + Ticket tRef ticket -> + let + ( ticket_, modal_ ) = + ProjectTicketPage.view + appContext + project.ref + tRef + ticket + in + { pageId = "project-page project-ticket-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader + ProjectPageHeader.Tickets + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectTicketPageMsg ticket_) + , modal = modal (Maybe.map (Html.map ProjectTicketPageMsg) modal_) + } + + Tickets tickets -> + let + ( tickets_, modal_ ) = + ProjectTicketsPage.view + appContext + project + tickets + in + { pageId = "project-page project-tickets-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader + ProjectPageHeader.Tickets + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectTicketsPageMsg tickets_) + , modal = modal (Maybe.map (Html.map ProjectTicketsPageMsg) modal_) + } + + Settings settings -> + let + settings_ = + ProjectSettingsPage.view appContext.session project settings + in + { pageId = "project-page project-settings-page" + , title = title + , appHeader = appHeader ViewMode.Regular + , pageHeader = + Just + (pageHeader + ProjectPageHeader.Settings + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectSettingsPageMsg settings_) + , modal = modal Nothing + } diff --git a/src/UnisonShare/Page/ProjectPageHeader.elm b/src/UnisonShare/Page/ProjectPageHeader.elm new file mode 100644 index 00000000..5e3026a7 --- /dev/null +++ b/src/UnisonShare/Page/ProjectPageHeader.elm @@ -0,0 +1,309 @@ +module UnisonShare.Page.ProjectPageHeader exposing (..) + +import Html exposing (Html, div, label, text) +import Html.Attributes exposing (class) +import UI.AnchoredOverlay exposing (AnchoredOverlay) +import UI.Button as Button +import UI.Click as Click exposing (Click) +import UI.Icon as Icon +import UI.Navigation as Nav +import UI.PageHeader as PageHeader exposing (PageHeader) +import UI.Tag as Tag +import UI.Tooltip as Tooltip +import UnisonShare.Link as Link +import UnisonShare.Project as Project exposing (Project, ProjectDetails) +import UnisonShare.Project.ProjectListing as ProjectListing +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Session as Session exposing (Session) + + +type ActiveNavItem + = NoActiveNavItem + | Overview + | DocsAndCode + | Tickets + | Contributions + | Releases + | Settings + + + +-- Create + + +empty : Project p -> PageHeader msg +empty project = + let + context = + { isActive = False + , click = Nothing + , content = + ProjectListing.projectListing project + |> ProjectListing.large + |> ProjectListing.subdued + |> ProjectListing.view + } + in + PageHeader.pageHeader context + + +loading : ProjectRef -> PageHeader msg +loading projectRef = + empty { ref = projectRef, visibility = Project.Public } + + +disabled : ProjectRef -> PageHeader msg +disabled projectRef = + empty { ref = projectRef, visibility = Project.Public } + + +error : ProjectRef -> PageHeader msg +error projectRef = + empty { ref = projectRef, visibility = Project.Public } + + +type alias NavItems msg = + { code : Nav.NavItem msg + , tickets : Nav.NavItem msg + , contributions : Nav.NavItem msg + , releases : Nav.NavItem msg + , settings : Nav.NavItem msg + } + + +allNavItems : ProjectDetails -> AnchoredOverlay msg -> NavItems msg +allNavItems project switchBranch = + let + defaultBranchRef = + Project.defaultBrowsingBranch project + + withContributionsCount navItem = + if project.numActiveContributions > 0 then + Nav.navItemWithTag (Tag.tag (String.fromInt project.numActiveContributions)) navItem + + else + navItem + + withTicketsCount navItem = + if project.numOpenTickets > 0 then + Nav.navItemWithTag (Tag.tag (String.fromInt project.numOpenTickets)) navItem + + else + navItem + in + { code = + Nav.navItem "Code" (Link.projectBranchRoot project.ref defaultBranchRef) + |> Nav.navItemWithIcon Icon.documentCode + |> Nav.navItemWithAnchoredOverlay switchBranch + , tickets = + Nav.navItem "Tickets" (Link.projectTickets project.ref) + |> Nav.navItemWithIcon Icon.bug + |> withTicketsCount + , contributions = + Nav.navItem "Contributions" (Link.projectContributions project.ref) + |> Nav.navItemWithIcon Icon.merge + |> withContributionsCount + , releases = + Nav.navItem "Releases" (Link.projectReleases project.ref) + |> Nav.navItemWithIcon Icon.rocket + , settings = + Nav.navItem "Settings" (Link.projectSettings project.ref) + |> Nav.navItemWithIcon Icon.cog + } + + +viewRightSide : Session -> msg -> msg -> ProjectDetails -> List (Html msg) +viewRightSide session toggleFavMsg useProjectButtonClickMsg project = + let + viewKpi icon num description = + div + [ class "kpi" ] + [ Tooltip.view + (div + [ class "kpi-content" ] + [ Icon.view icon + , label [ class "kpi-num" ] [ text (String.fromInt num) ] + ] + ) + (Tooltip.text description + |> Tooltip.tooltip + |> Tooltip.withArrow Tooltip.Middle + |> Tooltip.withPosition Tooltip.LeftOf + ) + ] + + favKpi p icon = + viewKpi + icon + p.numFavs + "Total number of favorites" + + viewFav p = + case ( session, p.isFaved ) of + ( Session.SignedIn _, Project.NotFaved ) -> + Click.view [ class "not-faved" ] + [ favKpi p Icon.heartOutline ] + (Click.onClick toggleFavMsg) + + ( Session.SignedIn _, Project.Faved ) -> + Click.view [ class "is-faved" ] + [ favKpi p Icon.heart ] + (Click.onClick toggleFavMsg) + + ( Session.SignedIn _, Project.JustFaved ) -> + Click.view [ class "is-faved just-faved" ] + [ favKpi p Icon.heart ] + (Click.onClick toggleFavMsg) + + _ -> + favKpi p Icon.heartOutline + in + [ div [ class "min-lg" ] + [ div [ class "kpis" ] + [ viewFav project ] + + {- Turn off release downloads for now + , viewKpi Icon.download + (Project.fourWeekTotalDownloads project) + "Total downloads for the last 4 weeks" + ] + -} + ] + , div [ class "min-lg" ] + [ Button.iconThenLabel useProjectButtonClickMsg Icon.download "Use Project" + |> Button.positive + |> Button.view + ] + , div [ class "max-lg" ] + [ Button.icon useProjectButtonClickMsg Icon.download + |> Button.positive + |> Button.view + ] + ] + + +type alias ProjectPageHeaderConfig msg = + { toggleFavMsg : msg + , useProjectButtonClickMsg : msg + , mobileNavToggleMsg : msg + , mobileNavIsOpen : Bool + , activeNavItem : ActiveNavItem + , switchBranch : AnchoredOverlay msg + , contextClick : Click msg + } + + +projectPageHeader : Session -> ProjectPageHeaderConfig msg -> ProjectDetails -> PageHeader msg +projectPageHeader session config project = + let + context = + { isActive = config.activeNavItem == Overview + , click = Just config.contextClick + , content = + ProjectListing.projectListing project + |> ProjectListing.withClick Link.userProfile Link.projectOverview + |> ProjectListing.large + |> ProjectListing.view + } + + allNavItems_ = + allNavItems project config.switchBranch + + nav = + case ( Session.hasProjectAccess project.ref session, config.activeNavItem ) of + ( True, DocsAndCode ) -> + Nav.withItems [] + allNavItems_.code + [ allNavItems_.tickets, allNavItems_.contributions, allNavItems_.releases, allNavItems_.settings ] + Nav.empty + + ( True, Tickets ) -> + Nav.withItems + [ allNavItems_.code ] + allNavItems_.tickets + [ allNavItems_.contributions, allNavItems_.releases, allNavItems_.settings ] + Nav.empty + + ( True, Contributions ) -> + Nav.withItems + [ allNavItems_.code, allNavItems_.tickets ] + allNavItems_.contributions + [ allNavItems_.releases, allNavItems_.settings ] + Nav.empty + + ( True, Releases ) -> + Nav.withItems + [ allNavItems_.code, allNavItems_.tickets, allNavItems_.contributions ] + allNavItems_.releases + [ allNavItems_.settings ] + Nav.empty + + ( True, Settings ) -> + Nav.withItems + [ allNavItems_.code, allNavItems_.tickets, allNavItems_.contributions, allNavItems_.releases ] + allNavItems_.settings + [] + Nav.empty + + ( True, _ ) -> + Nav.withNoSelectedItems + [ allNavItems_.code, allNavItems_.tickets, allNavItems_.contributions, allNavItems_.releases, allNavItems_.settings ] + Nav.empty + + ( _, DocsAndCode ) -> + Nav.withItems + [] + allNavItems_.code + [ allNavItems_.tickets, allNavItems_.contributions, allNavItems_.releases ] + Nav.empty + + ( _, Tickets ) -> + Nav.withItems + [ allNavItems_.code ] + allNavItems_.tickets + [ allNavItems_.contributions, allNavItems_.releases ] + Nav.empty + + ( _, Contributions ) -> + Nav.withItems + [ allNavItems_.code, allNavItems_.tickets ] + allNavItems_.contributions + [ allNavItems_.releases ] + Nav.empty + + ( _, Releases ) -> + Nav.withItems + [ allNavItems_.code, allNavItems_.tickets, allNavItems_.contributions ] + allNavItems_.releases + [] + Nav.empty + + _ -> + Nav.withNoSelectedItems + [ allNavItems_.code, allNavItems_.tickets, allNavItems_.contributions, allNavItems_.releases ] + Nav.empty + + pageHeader_ = + context + |> PageHeader.pageHeader + |> PageHeader.withNavigation + { navigation = nav + , mobileNavToggleMsg = config.mobileNavToggleMsg + , mobileNavIsOpen = config.mobileNavIsOpen + } + + pageHeader = + if config.activeNavItem /= Overview then + pageHeader_ + |> PageHeader.withRightSide + (viewRightSide + session + config.toggleFavMsg + config.useProjectButtonClickMsg + project + ) + + else + pageHeader_ + in + pageHeader diff --git a/src/UnisonShare/Page/ProjectReleasePage.elm b/src/UnisonShare/Page/ProjectReleasePage.elm new file mode 100644 index 00000000..ad28be82 --- /dev/null +++ b/src/UnisonShare/Page/ProjectReleasePage.elm @@ -0,0 +1,433 @@ +module UnisonShare.Page.ProjectReleasePage exposing (..) + +import Browser.Dom as Dom +import Code.Definition.Doc as Doc exposing (Doc) +import Code.Hash as Hash +import Code.Perspective as Perspective +import Code.ProjectSlug as ProjectSlug +import Code.Version as Version exposing (Version) +import Html exposing (Html, div, p, section, span, strong, text) +import Html.Attributes exposing (class, classList, id) +import Http +import Json.Decode as Decode +import Lib.HttpApi as HttpApi +import RemoteData exposing (RemoteData(..), WebData) +import Task +import UI +import UI.Button as Button +import UI.ByAt as ByAt +import UI.Card as Card +import UI.CopyField as CopyField +import UI.Divider as Divider +import UI.Icon as Icon +import UI.Modal as Modal +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle exposing (PageTitle) +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext as AppContext exposing (AppContext) +import UnisonShare.CodeBrowsingContext as CodeBrowsingContext +import UnisonShare.InteractiveDoc as InteractiveDoc +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Project.Release as Release exposing (Release) +import UnisonShare.Route as Route + + + +-- MODEL + + +type ProjectReleaseModal + = NoModal + | InstallModal Version + + +type DocVisibility + = Unknown + | Cropped + | NotCropped + | MadeFullyVisible + + +type alias ReleaseNotes = + { doc : Doc + , interactiveDoc : InteractiveDoc.Model + , docVisibility : DocVisibility + } + + +type alias Model = + { release : WebData Release + , releaseNotes : WebData (Maybe ReleaseNotes) + , modal : ProjectReleaseModal + } + + +init : AppContext -> ProjectRef -> Version -> ( Model, Cmd Msg ) +init appContext projectRef version = + ( { release = Loading + , releaseNotes = Loading + , modal = NoModal + } + , Cmd.batch + [ fetchRelease appContext projectRef version + , fetchReleaseNotes appContext projectRef version + ] + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchReleaseFinished (WebData Release) + | FetchReleaseNotesFinished (WebData (Maybe Doc)) + | ShowInstallModal Version + | CloseModal + | InteractiveDocMsg InteractiveDoc.Msg + | IsReleaseNotesCropped (Result Dom.Error Bool) + | ShowFullReleaseNotes + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef msg model = + case msg of + FetchReleaseFinished release -> + ( { model | release = release }, Cmd.none ) + + FetchReleaseNotesFinished doc -> + let + releaseNotes = + RemoteData.map + (Maybe.map (\d -> { doc = d, interactiveDoc = InteractiveDoc.init, docVisibility = Unknown })) + doc + in + ( { model | releaseNotes = releaseNotes }, isReleaseNotesCropped ) + + ShowInstallModal projectVersion -> + ( { model | modal = InstallModal projectVersion }, Cmd.none ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none ) + + InteractiveDocMsg dMsg -> + case ( model.release, model.releaseNotes ) of + ( Success release, Success (Just rn) ) -> + let + config = + AppContext.toCodeConfig + appContext + (CodeBrowsingContext.project projectRef (Release.branchRef release)) + Perspective.relativeRootPerspective + + ( interactiveDoc, cmd, iOutMsg ) = + InteractiveDoc.update config dMsg rn.interactiveDoc + + navCmd = + case iOutMsg of + InteractiveDoc.OpenDefinition ref -> + Route.navigate + appContext.navKey + (Route.projectBranchDefinition projectRef + (Release.branchRef release) + Perspective.relativeRootPerspective + ref + ) + + _ -> + Cmd.none + in + ( { model + | releaseNotes = + Success (Just { rn | interactiveDoc = interactiveDoc }) + } + , Cmd.batch [ Cmd.map InteractiveDocMsg cmd, navCmd ] + ) + + _ -> + ( model, Cmd.none ) + + IsReleaseNotesCropped r -> + let + visibility = + case r of + Ok True -> + Cropped + + Ok False -> + NotCropped + + -- If we can't tell, better make it fully visible, than Unknown + Err _ -> + MadeFullyVisible + in + ( { model + | releaseNotes = + RemoteData.map + (Maybe.map (\rn -> { rn | docVisibility = visibility })) + model.releaseNotes + } + , Cmd.none + ) + + ShowFullReleaseNotes -> + ( { model + | releaseNotes = + RemoteData.map + (Maybe.map (\rn -> { rn | docVisibility = MadeFullyVisible })) + model.releaseNotes + } + , Cmd.none + ) + + NoOp -> + ( model, Cmd.none ) + + + +-- EFFECTS + + +isReleaseNotesCropped : Cmd Msg +isReleaseNotesCropped = + Dom.getViewportOf "release-notes_container" + |> Task.map (\v -> v.viewport.height < v.scene.height) + |> Task.attempt IsReleaseNotesCropped + + +fetchRelease : AppContext -> ProjectRef -> Version -> Cmd Msg +fetchRelease appContext projectRef version = + ShareApi.projectRelease projectRef version + |> HttpApi.toRequest Release.decode (RemoteData.fromResult >> FetchReleaseFinished) + |> HttpApi.perform appContext.api + + +fetchReleaseNotes : AppContext -> ProjectRef -> Version -> Cmd Msg +fetchReleaseNotes appContext projectRef version = + ShareApi.projectReleaseNotes projectRef version + |> HttpApi.toRequest + (Decode.field "doc" (Decode.nullable Doc.decode)) + (RemoteData.fromResult >> FetchReleaseNotesFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +pageTitle : Version -> PageTitle Msg +pageTitle version = + PageTitle.title ("Release " ++ Version.toString version) + + +detailedPageTitle : AppContext -> Release -> PageTitle Msg +detailedPageTitle appContext release = + let + byAt = + case release.status of + Release.Published p -> + ByAt.view + appContext.timeZone + appContext.now + (ByAt.handleOnly p.by p.at) + + _ -> + UI.nothing + in + pageTitle release.version + |> PageTitle.withDescription_ + (span [ class "project-release_page-title_description" ] + [ Hash.view release.causalHashSquashed, byAt ] + ) + |> PageTitle.withRightSide + [ div [ class "project-release_actions" ] + [ Button.iconThenLabel_ (Link.projectBranchRoot release.projectRef (Release.branchRef release)) Icon.browse "Browse Code" + |> Button.medium + |> Button.view + , Button.iconThenLabel (ShowInstallModal release.version) Icon.download "Install" + |> Button.positive + |> Button.medium + |> Button.view + ] + ] + + +viewLoadingPage : Version -> PageLayout Msg +viewLoadingPage version = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ div [ class "project-releases_releases" ] + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + ] + |> PageContent.withPageTitle (pageTitle version) + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewErrorPage : Version -> Http.Error -> PageLayout Msg +viewErrorPage version _ = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn + [ Card.card + [ StatusBanner.bad "Something broke on our end and we couldn't show the project release. Please try again." + ] + |> Card.withClassName "project-releases_error" + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (pageTitle version) + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewReleaseNotes : ReleaseNotes -> Html Msg +viewReleaseNotes releaseNotes = + let + ( showFullDoc, shownInFull ) = + case releaseNotes.docVisibility of + Unknown -> + ( UI.nothing, False ) + + Cropped -> + ( div [ class "show-full-release-notes" ] + [ Button.iconThenLabel ShowFullReleaseNotes Icon.arrowDown "Show full release notes" + |> Button.small + |> Button.view + ] + , False + ) + + _ -> + ( UI.nothing, True ) + + classes = + classList + [ ( "project-release-details_release-notes", True ) + , ( "shown-in-full", shownInFull ) + ] + in + section [ classes ] + [ div [ id "release-notes_container" ] + [ Html.map InteractiveDocMsg + (InteractiveDoc.view + releaseNotes.interactiveDoc + releaseNotes.doc + ) + ] + , showFullDoc + ] + + +viewPageContent : AppContext -> Release -> Maybe ReleaseNotes -> PageContent Msg +viewPageContent appContext release releaseNotes = + let + content = + case releaseNotes of + Just rn -> + [ Card.card + [ viewReleaseNotes rn + ] + |> Card.asContained + |> Card.view + ] + + Nothing -> + [] + in + PageContent.oneColumn content + |> PageContent.withPageTitle (detailedPageTitle appContext release) + + +viewInstallModal : ProjectRef -> Version -> Html Msg +viewInstallModal projectRef version = + let + projectRef_ = + ProjectRef.toString projectRef + + pullCommand = + "pull " + ++ projectRef_ + ++ "/releases/" + ++ Version.toString version + ++ " lib." + ++ ProjectSlug.toNamespaceString (ProjectRef.slug projectRef) + ++ "_" + ++ Version.toNamespaceString version + + content = + Modal.Content + (div [ class "use-project-modal-content" ] + [ p [] + [ text "From within your project in UCM, run the " + , strong [] [ text "pull" ] + , text " command:" + ] + , CopyField.copyField (\_ -> NoOp) pullCommand |> CopyField.withPrefix "myProject/main>" |> CopyField.view + , div [ class "hint" ] [ text "Copy and paste this command into UCM." ] + , Divider.divider |> Divider.small |> Divider.view + , div [ class "action" ] [ Button.iconThenLabel CloseModal Icon.thumbsUp "Got it!" |> Button.emphasized |> Button.view ] + ] + ) + in + Modal.modal "use-project-modal" CloseModal content + |> Modal.withHeader ("Install " ++ projectRef_) + |> Modal.view + + +view : AppContext -> ProjectRef -> Version -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext projectRef version model = + let + data = + RemoteData.map2 (\rl rn -> ( rl, rn )) + model.release + model.releaseNotes + in + case data of + NotAsked -> + ( viewLoadingPage version, Nothing ) + + Loading -> + ( viewLoadingPage version, Nothing ) + + Success ( release, releaseNotes ) -> + let + modal = + case model.modal of + NoModal -> + Nothing + + InstallModal v -> + Just (viewInstallModal projectRef v) + in + ( PageLayout.centeredNarrowLayout + (viewPageContent appContext release releaseNotes) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Failure e -> + ( viewErrorPage version e, Nothing ) diff --git a/src/UnisonShare/Page/ProjectReleasesPage.elm b/src/UnisonShare/Page/ProjectReleasesPage.elm new file mode 100644 index 00000000..0216e473 --- /dev/null +++ b/src/UnisonShare/Page/ProjectReleasesPage.elm @@ -0,0 +1,890 @@ +module UnisonShare.Page.ProjectReleasesPage exposing (..) + +import Browser.Dom as Dom +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.Definition.Doc as Doc exposing (Doc) +import Code.Hash as Hash exposing (Hash) +import Code.Perspective as Perspective +import Code.ProjectSlug as ProjectSlug +import Code.Version as Version exposing (Version) +import Html exposing (Html, div, footer, h1, h2, header, p, section, strong, text) +import Html.Attributes exposing (class, classList, id) +import Http +import Json.Decode as Decode +import Lib.HttpApi as HttpApi +import Lib.Util as Util +import List.Nonempty as NEL +import Maybe.Extra as MaybeE +import RemoteData exposing (RemoteData(..), WebData) +import Task +import UI +import UI.Button as Button exposing (Button) +import UI.ByAt as ByAt +import UI.Card as Card +import UI.Click as Click +import UI.CopyField as CopyField +import UI.Divider as Divider +import UI.EmptyState as EmptyState +import UI.EmptyStateCard as EmptyStateCard +import UI.Icon as Icon +import UI.Modal as Modal +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle exposing (PageTitle) +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UI.Tag as Tag +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext as AppContext exposing (AppContext) +import UnisonShare.BranchSummary as BranchSummary exposing (BranchSummary) +import UnisonShare.CodeBrowsingContext as CodeBrowsingContext +import UnisonShare.InteractiveDoc as InteractiveDoc +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Project.Release as Release exposing (Release) +import UnisonShare.PublishProjectReleaseModal as PublishProjectReleaseModal +import UnisonShare.Route as Route +import UnisonShare.Session as Session + + + +-- MODEL + + +type ProjectReleasesModal + = NoModal + | InstallModal Version + | PublishReleaseModal PublishProjectReleaseModal.Model + + +type Releases + = NoReleases + | Releases + { -- latest is a Maybe because even though we might have releases, + -- latest has to be the latest _published_ release, and its possible + -- to have a few unpublished releases, without a published one. + latest : Maybe Release + , past : List Release + } + + +type alias ReleaseDraft = + { branch : BranchSummary + , version : Version + } + + +type ProjectReleasesPermissions a + = CanPublish a + | NotPermitted + + +type DocVisibility + = Unknown + | Cropped + | NotCropped + | MadeFullyVisible + + +type alias ReleaseNotes = + { doc : Doc + , interactiveDoc : InteractiveDoc.Model + , docVisibility : DocVisibility + } + + +type alias Model = + { releases : WebData Releases + , latestReleaseNotes : WebData (Maybe ReleaseNotes) + , permitted : ProjectReleasesPermissions (WebData (List ReleaseDraft)) + , modal : ProjectReleasesModal + } + + +init : AppContext -> ProjectRef -> WebData (Maybe Version) -> ( Model, Cmd Msg ) +init appContext projectRef latestVersion = + let + ( permitted, releaseDraftsCmd ) = + if Session.hasProjectAccess projectRef appContext.session then + ( CanPublish Loading, fetchReleaseDrafts appContext projectRef ) + + else + ( NotPermitted, Cmd.none ) + + ( latestReleaseNotes, latestReleaseNotesCmd ) = + case latestVersion of + Success (Just v) -> + ( Loading + , fetchReleaseNotes + FetchLatestReleaseNotesFinished + appContext + projectRef + v + ) + + Success Nothing -> + ( Success Nothing, Cmd.none ) + + _ -> + ( NotAsked, Cmd.none ) + in + ( { releases = Loading + , latestReleaseNotes = latestReleaseNotes + , permitted = permitted + , modal = NoModal + } + , Cmd.batch + [ fetchProjectReleases appContext projectRef + , releaseDraftsCmd + , latestReleaseNotesCmd + ] + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchProjectReleasesFinished (WebData (List Release)) + | FetchReleaseDraftsFinished (WebData (List BranchSummary)) + | FetchLatestReleaseNotesFinished (WebData (Maybe Doc)) + | ShowPublishReleaseModal (Maybe BranchSummary) + | ShowInstallModal Version + | CloseModal + | InteractiveDocMsg InteractiveDoc.Msg + | PublishProjectReleaseModalMsg PublishProjectReleaseModal.Msg + | IsReleaseNotesCropped (Result Dom.Error Bool) + | ShowFullReleaseNotes + + +type OutMsg + = None + | PublishedNewRelease Release + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef msg model = + case msg of + FetchProjectReleasesFinished data -> + ( { model | releases = RemoteData.map toProjectReleases data } + , Cmd.none + , None + ) + + FetchReleaseDraftsFinished draftBranches -> + case model.permitted of + CanPublish _ -> + let + toDraft branchSummary acc = + case branchSummary.ref of + BranchRef.ReleaseDraftBranchRef v -> + { version = v, branch = branchSummary } :: acc + + _ -> + acc + + toDrafts brs = + brs + |> List.foldl toDraft [] + |> Util.sortByWith .version Version.descending + in + ( { model | permitted = CanPublish (RemoteData.map toDrafts draftBranches) }, Cmd.none, None ) + + NotPermitted -> + ( model, Cmd.none, None ) + + FetchLatestReleaseNotesFinished doc -> + let + releaseNotes = + RemoteData.map + (Maybe.map (\d -> { doc = d, interactiveDoc = InteractiveDoc.init, docVisibility = Unknown })) + doc + in + ( { model | latestReleaseNotes = releaseNotes }, isReleaseNotesCropped, None ) + + ShowPublishReleaseModal draftBranch -> + case model.releases of + Success rs -> + let + ( modal_, cmd ) = + PublishProjectReleaseModal.init + appContext + projectRef + (currentVersion rs) + draftBranch + in + ( { model | modal = PublishReleaseModal modal_ } + , Cmd.map PublishProjectReleaseModalMsg cmd + , None + ) + + _ -> + ( model, Cmd.none, None ) + + ShowInstallModal projectVersion -> + ( { model | modal = InstallModal projectVersion }, Cmd.none, None ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none, None ) + + PublishProjectReleaseModalMsg pprmMsg -> + case model.modal of + PublishReleaseModal m -> + let + ( m_, cmd, out ) = + PublishProjectReleaseModal.update appContext projectRef pprmMsg m + + ( newModel, notesCmd, releasesOut ) = + case out of + PublishProjectReleaseModal.NoOutMsg -> + ( { model | modal = PublishReleaseModal m_ }, Cmd.none, None ) + + PublishProjectReleaseModal.Published latest -> + let + past_ rs = + case rs of + NoReleases -> + [] + + Releases rs_ -> + case rs_.latest of + Just r -> + r :: rs_.past + + _ -> + rs_.past + + past = + model.releases + |> RemoteData.map past_ + |> RemoteData.withDefault [] + + releases = + { latest = Just latest + , past = past + } + in + ( { model | releases = Success (Releases releases), modal = PublishReleaseModal m_ } + , fetchReleaseNotes FetchLatestReleaseNotesFinished appContext projectRef latest.version + , PublishedNewRelease latest + ) + + PublishProjectReleaseModal.RequestCloseModal -> + ( { model | modal = NoModal }, Cmd.none, None ) + in + ( newModel, Cmd.batch [ Cmd.map PublishProjectReleaseModalMsg cmd, notesCmd ], releasesOut ) + + _ -> + ( model, Cmd.none, None ) + + InteractiveDocMsg dMsg -> + case ( model.releases, model.latestReleaseNotes ) of + ( Success (Releases releases), Success (Just rn) ) -> + case releases.latest of + Just latest -> + let + config = + AppContext.toCodeConfig + appContext + (CodeBrowsingContext.project projectRef (Release.branchRef latest)) + Perspective.relativeRootPerspective + + ( interactiveDoc, cmd, iOutMsg ) = + InteractiveDoc.update config dMsg rn.interactiveDoc + + navCmd = + case iOutMsg of + InteractiveDoc.OpenDefinition ref -> + Route.navigate + appContext.navKey + (Route.projectBranchDefinition projectRef + (Release.branchRef latest) + Perspective.relativeRootPerspective + ref + ) + + _ -> + Cmd.none + in + ( { model + | latestReleaseNotes = + Success (Just { rn | interactiveDoc = interactiveDoc }) + } + , Cmd.batch [ Cmd.map InteractiveDocMsg cmd, navCmd ] + , None + ) + + _ -> + ( model, Cmd.none, None ) + + _ -> + ( model, Cmd.none, None ) + + IsReleaseNotesCropped r -> + let + visibility = + case r of + Ok True -> + Cropped + + Ok False -> + NotCropped + + -- If we can't tell, better make it fully visible, than Unknown + Err _ -> + MadeFullyVisible + in + ( { model + | latestReleaseNotes = + RemoteData.map + (Maybe.map (\rn -> { rn | docVisibility = visibility })) + model.latestReleaseNotes + } + , Cmd.none + , None + ) + + ShowFullReleaseNotes -> + ( { model + | latestReleaseNotes = + RemoteData.map + (Maybe.map (\rn -> { rn | docVisibility = MadeFullyVisible })) + model.latestReleaseNotes + } + , Cmd.none + , None + ) + + NoOp -> + ( model, Cmd.none, None ) + + +updateWithNoLatestReleaseNotes : Model -> Model +updateWithNoLatestReleaseNotes model = + { model | latestReleaseNotes = Success Nothing } + + + +-- HELPERS + + +currentVersion : Releases -> Version +currentVersion releases = + let + unreleased = + Version.empty + in + case releases of + NoReleases -> + unreleased + + Releases rs -> + rs.latest + |> Maybe.map .version + |> Maybe.withDefault unreleased + + +toProjectReleases : List Release -> Releases +toProjectReleases releases = + let + sort = + Util.sortByWith .version Version.descending + in + case sort releases of + latest :: past -> + if Release.isPublished latest then + Releases + { latest = Just latest + , past = past + } + + else + Releases + { latest = Nothing + , past = latest :: past + } + + _ -> + NoReleases + + + +-- EFFECTS + + +isReleaseNotesCropped : Cmd Msg +isReleaseNotesCropped = + Dom.getViewportOf "release-notes_container" + |> Task.map (\v -> v.viewport.height < v.scene.height) + |> Task.attempt IsReleaseNotesCropped + + +fetchProjectReleases : AppContext -> ProjectRef -> Cmd Msg +fetchProjectReleases appContext projectRef = + ShareApi.projectReleases projectRef + |> HttpApi.toRequest + (Decode.field "items" (Decode.list Release.decode)) + (RemoteData.fromResult >> FetchProjectReleasesFinished) + |> HttpApi.perform appContext.api + + +fetchReleaseDrafts : AppContext -> ProjectRef -> Cmd Msg +fetchReleaseDrafts appContext projectRef = + let + params = + { kind = ShareApi.ProjectBranches + , searchQuery = Just "releases/drafts/" + , limit = 10 + , cursor = Nothing + } + in + ShareApi.projectBranches projectRef params + |> HttpApi.toRequest + (Decode.field "items" (Decode.list BranchSummary.decode)) + (RemoteData.fromResult >> FetchReleaseDraftsFinished) + |> HttpApi.perform appContext.api + + +fetchLatestReleaseNotesAndUpdate : AppContext -> ProjectRef -> Model -> Version -> ( Model, Cmd Msg ) +fetchLatestReleaseNotesAndUpdate appContext projectRef model version = + ( { model | latestReleaseNotes = Loading } + , fetchReleaseNotes FetchLatestReleaseNotesFinished appContext projectRef version + ) + + +fetchReleaseNotes : (WebData (Maybe Doc) -> Msg) -> AppContext -> ProjectRef -> Version -> Cmd Msg +fetchReleaseNotes doneMsg appContext projectRef version = + ShareApi.projectReleaseNotes projectRef version + |> HttpApi.toRequest + (Decode.field "doc" (Decode.nullable Doc.decode)) + (RemoteData.fromResult >> doneMsg) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +pageTitle : ProjectRef -> Maybe (Button Msg) -> PageTitle Msg +pageTitle projectRef action = + let + rightSide = + case action of + Just a -> + [ Button.view a ] + + Nothing -> + [] + + pageDescription = + "Explore latest and past releases of " ++ ProjectRef.toString projectRef + in + PageTitle.title "Releases" + |> PageTitle.withDescription pageDescription + |> PageTitle.withRightSide rightSide + + +viewLoadingPage : ProjectRef -> PageLayout Msg +viewLoadingPage projectRef = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ div [ class "project-releases_releases" ] + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + , div [ class "project-releases_loading_past-releases" ] + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + , shape Placeholder.Large + ] + ] + ] + |> PageContent.withPageTitle (pageTitle projectRef Nothing) + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewErrorPage : ProjectRef -> Http.Error -> PageLayout Msg +viewErrorPage projectRef _ = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn + [ Card.card + [ StatusBanner.bad "Something broke on our end and we couldn't show the project releases. Please try again." + ] + |> Card.withClassName "project-releases_error" + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (pageTitle projectRef Nothing) + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewEmptyState : ProjectRef -> List (Html msg) +viewEmptyState projectRef = + let + emptyState = + EmptyState.iconCloud (EmptyState.CircleCenterPiece (text "🐣")) + |> EmptyState.withContent + [ p + [ class "no-releases" ] + [ text "There are no " + , strong [] [ text (ProjectRef.toString projectRef) ] + , text " releases yet." + ] + , p [] [ text "Check back later." ] + ] + in + [ EmptyStateCard.view emptyState + ] + + +viewLatestReleaseNotes : ReleaseNotes -> Html Msg +viewLatestReleaseNotes releaseNotes = + let + ( showFullDoc, shownInFull ) = + case releaseNotes.docVisibility of + Unknown -> + ( UI.nothing, False ) + + Cropped -> + ( div [ class "show-full-release-notes" ] + [ Button.iconThenLabel ShowFullReleaseNotes Icon.arrowDown "Show full release notes" + |> Button.small + |> Button.view + ] + , False + ) + + _ -> + ( UI.nothing, True ) + + classes = + classList + [ ( "project-release-details_release-notes", True ) + , ( "shown-in-full", shownInFull ) + ] + in + section [ classes ] + [ div [ id "release-notes_container" ] + [ Html.map InteractiveDocMsg + (InteractiveDoc.view + releaseNotes.interactiveDoc + releaseNotes.doc + ) + ] + , showFullDoc + ] + + +viewLatestRelease : AppContext -> ProjectRef -> Release -> Maybe ReleaseNotes -> Html Msg +viewLatestRelease appContext projectRef release releaseNotes = + let + doc = + releaseNotes + |> Maybe.map viewLatestReleaseNotes + |> Maybe.withDefault UI.nothing + + byAt = + case release.status of + Release.Published p -> + ByAt.view + appContext.timeZone + appContext.now + (ByAt.handleOnly p.by p.at) + + _ -> + UI.nothing + in + Card.card + [ header [] + [ div [ class "project-release-details_version-hash" ] + [ h1 [] + [ Click.view [] + [ Version.view release.version ] + (Link.projectRelease projectRef release.version) + ] + , viewBranchHash + release.projectRef + (Release.branchRef release) + release.causalHashSquashed + ] + , byAt + ] + , doc + , footer [] + [ Button.iconThenLabel_ (Link.projectBranchRoot release.projectRef (Release.branchRef release)) Icon.browse "Browse" + |> Button.medium + |> Button.view + , Button.iconThenLabel (ShowInstallModal release.version) Icon.download "Install" + |> Button.positive + |> Button.medium + |> Button.view + ] + ] + |> Card.asContained + |> Card.withClassName "project-release-details" + |> Card.view + + +viewPastReleases : AppContext -> ProjectRef -> List Release -> List (Html msg) +viewPastReleases appContext projectRef releases = + let + status r = + case r.status of + Release.Published _ -> + Tag.tag "Published" + |> Tag.view + + Release.Unpublished _ -> + Tag.tag "Unpublished" + |> Tag.view + + _ -> + UI.nothing + + byAt r = + case r.status of + Release.Published p -> + ByAt.view + appContext.timeZone + appContext.now + (ByAt.handleOnly p.by p.at) + + Release.Unpublished p -> + ByAt.view + appContext.timeZone + appContext.now + (ByAt.handleOnly p.by p.at) + + _ -> + UI.nothing + + viewRelease r = + div [ class "project-releases_past-releases_release" ] + [ div [ class "project-releases_past-releases_release_version-and-hash" ] + [ Click.view [] [ Version.view r.version ] (Link.projectRelease projectRef r.version) + , viewBranchHash r.projectRef (Release.branchRef r) r.causalHashSquashed + ] + , div [ class "project-releases_past-releases_release_status-and-by-at" ] + [ status r, byAt r ] + ] + in + [ div [ class "project-releases_past-releases_group" ] + [ header [] + [ h2 [] [ text "Past Releases" ] + , Divider.divider |> Divider.small |> Divider.withoutMargin |> Divider.view + ] + , div [ class "project-releases_past-releases" ] + (List.map viewRelease releases) + ] + ] + + +{-| TODO: should this be in ui-core? +-} +viewBranchHash : ProjectRef -> BranchRef -> Hash -> Html msg +viewBranchHash projectRef branchRef hash = + let + link = + Link.projectBranchRoot projectRef branchRef + in + Click.view + [ class "browsable-branch-hash" ] + [ Hash.view hash, Icon.view Icon.browse ] + link + + +viewDraft : ReleaseDraft -> Html Msg +viewDraft draft = + let + br = + draft.branch + in + div [ class "release-draft" ] + [ div [ class "release-draft_meta" ] + [ Icon.view Icon.writingPad + , Version.view draft.version + , text "Release Draft" + , BranchRef.toTag br.ref |> Tag.view + , viewBranchHash br.project.ref br.ref br.causalHash + ] + , Button.iconThenLabel (ShowPublishReleaseModal (Just br)) Icon.rocket "Publish" + |> Button.emphasized + |> Button.small + |> Button.view + ] + + +viewPageContent : + AppContext + -> ProjectRef + -> Releases + -> Maybe ReleaseNotes + -> ProjectReleasesPermissions (List ReleaseDraft) + -> PageContent Msg +viewPageContent appContext projectRef releases latestReleaseNotes permitted = + let + content = + case releases of + NoReleases -> + viewEmptyState projectRef + + Releases rs -> + case rs.latest of + Just l -> + if List.isEmpty rs.past then + [ viewLatestRelease appContext projectRef l latestReleaseNotes ] + + else + viewLatestRelease appContext projectRef l latestReleaseNotes + :: viewPastReleases appContext projectRef rs.past + + Nothing -> + viewPastReleases appContext projectRef rs.past + + currentVersion_ = + currentVersion releases + + ( drafts, publishAction ) = + case permitted of + CanPublish d -> + ( d + |> List.filter (.version >> Version.lessThan currentVersion_) + |> NEL.fromList + |> Maybe.map (NEL.map viewDraft) + |> Maybe.map NEL.toList + |> Maybe.map Card.card + |> Maybe.map (Card.withClassName "project-releases_drafts") + |> Maybe.map Card.asContained + |> Maybe.map Card.withTightPadding + |> MaybeE.unwrap UI.nothing Card.view + , Button.iconThenLabel (ShowPublishReleaseModal Nothing) Icon.rocket "Cut a new release" + |> Button.outlined + |> Button.small + |> Just + ) + + NotPermitted -> + ( UI.nothing, Nothing ) + in + PageContent.oneColumn + [ div [ class "project-releases_releases" ] (drafts :: content) ] + |> PageContent.withPageTitle (pageTitle projectRef publishAction) + + +viewInstallModal : ProjectRef -> Version -> Html Msg +viewInstallModal projectRef version = + let + projectRef_ = + ProjectRef.toString projectRef + + libVersion = + "lib." + ++ ProjectSlug.toNamespaceString (ProjectRef.slug projectRef) + ++ "_" + ++ Version.toNamespaceString version + + pullCommand = + "pull " + ++ projectRef_ + ++ "/releases/" + ++ Version.toString version + ++ " " + ++ libVersion + + content = + Modal.Content + (div [ class "instruction" ] + [ p [] + [ text "From within your project in UCM, run the " + , strong [] [ text "pull" ] + , text " command:" + ] + , CopyField.copyField (\_ -> NoOp) pullCommand |> CopyField.withPrefix "myProject/main>" |> CopyField.view + , div [ class "hint" ] [ text "Copy and paste this command into UCM." ] + , div [ class "upgrade-hint" ] + [ div [ class "upgrade-icon" ] [ Icon.view Icon.arrowUp ] + , div [ class "upgrade-hint_content" ] + [ div [] [ text "Upgrading from a previous version? Pull using the above and then run:" ] + , div [ class "monospace" ] [ text ("myProject/main> upgrade " ++ libVersion) ] + ] + ] + , Divider.divider |> Divider.small |> Divider.view + , div [ class "action" ] [ Button.iconThenLabel CloseModal Icon.thumbsUp "Got it!" |> Button.emphasized |> Button.view ] + ] + ) + in + Modal.modal "use-project-modal" CloseModal content + |> Modal.withHeader ("Install " ++ projectRef_) + |> Modal.view + + +view : AppContext -> ProjectRef -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext projectRef model = + let + drafts = + case model.permitted of + CanPublish d -> + RemoteData.map CanPublish d + + NotPermitted -> + Success NotPermitted + + data = + RemoteData.map3 (\rs rn drafts_ -> ( rs, rn, drafts_ )) + model.releases + model.latestReleaseNotes + drafts + in + case data of + NotAsked -> + ( viewLoadingPage projectRef, Nothing ) + + Loading -> + ( viewLoadingPage projectRef, Nothing ) + + Success ( rs, latestReleaseNotes, drafts_ ) -> + let + modal = + case model.modal of + NoModal -> + Nothing + + InstallModal v -> + Just (viewInstallModal projectRef v) + + PublishReleaseModal pprm -> + Just + (Html.map + PublishProjectReleaseModalMsg + (PublishProjectReleaseModal.view (currentVersion rs) pprm) + ) + in + ( PageLayout.centeredNarrowLayout + (viewPageContent appContext projectRef rs latestReleaseNotes drafts_) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Failure e -> + ( viewErrorPage projectRef e, Nothing ) diff --git a/src/UnisonShare/Page/ProjectSettingsPage.elm b/src/UnisonShare/Page/ProjectSettingsPage.elm new file mode 100644 index 00000000..e133d2c0 --- /dev/null +++ b/src/UnisonShare/Page/ProjectSettingsPage.elm @@ -0,0 +1,299 @@ +module UnisonShare.Page.ProjectSettingsPage exposing (..) + +import Html exposing (div, footer, h2, text) +import Html.Attributes exposing (class) +import Http exposing (Error) +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import List.Nonempty as NEL +import RemoteData exposing (WebData) +import UI +import UI.Button as Button +import UI.Card as Card +import UI.ErrorCard as ErrorCard +import UI.Form.RadioField as RadioField +import UI.Icon as Icon +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project exposing (ProjectDetails, ProjectVisibility(..)) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Session as Session exposing (Session) + + + +-- MODEL + + +type alias Changes = + { visibility : ProjectVisibility } + + +type Form + = NoChanges + | WithChanges Changes + | Saving Changes + | SaveSuccessful + | SaveFailed Changes Error + + +type alias DeleteProject = + { confirmText : String, deleting : WebData () } + + +type alias Model = + { form : Form } + + +init : Model +init = + { form = NoChanges } + + + +-- UPDATE + + +type Msg + = UpdateVisibility ProjectVisibility + | DiscardChanges + | SaveChanges + | SaveFinished (HttpResult ()) + | ClearAfterSave + | ShowDeleteProjectModal + | CloseModal + + +type OutMsg + = None + | ProjectUpdated ProjectDetails + | ShowDeleteProjectModalRequest + + +update : AppContext -> ProjectDetails -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext project msg model = + case ( msg, model.form ) of + ( UpdateVisibility newVisibility, WithChanges c ) -> + if newVisibility /= project.visibility then + ( { model | form = WithChanges { c | visibility = newVisibility } }, Cmd.none, None ) + + else + ( { model | form = NoChanges }, Cmd.none, None ) + + ( UpdateVisibility newVisibility, _ ) -> + ( { model | form = WithChanges { visibility = newVisibility } }, Cmd.none, None ) + + ( DiscardChanges, WithChanges _ ) -> + ( { model | form = NoChanges }, Cmd.none, None ) + + ( SaveChanges, WithChanges c ) -> + ( { model | form = Saving c }, updateProjectSettings appContext project.ref c, None ) + + ( SaveFinished (Ok _), Saving c ) -> + let + p = + { project | visibility = c.visibility } + in + ( { model | form = SaveSuccessful }, Util.delayMsg 3000 ClearAfterSave, ProjectUpdated p ) + + ( SaveFinished (Err e), Saving c ) -> + ( { model | form = SaveFailed c e }, Cmd.none, None ) + + ( ClearAfterSave, SaveSuccessful ) -> + ( { model | form = NoChanges }, Cmd.none, None ) + + ( ShowDeleteProjectModal, _ ) -> + ( model, Cmd.none, ShowDeleteProjectModalRequest ) + + _ -> + ( model, Cmd.none, None ) + + + +-- EFFECTS + + +updateProjectSettings : AppContext -> ProjectRef -> Changes -> Cmd Msg +updateProjectSettings appContext projectRef changes = + ShareApi.updateProject projectRef (ShareApi.ProjectSettingsUpdate changes) + |> HttpApi.toRequestWithEmptyResponse SaveFinished + |> HttpApi.perform appContext.api + + + +-- VIEW + + +pageTitle : PageTitle.PageTitle msg +pageTitle = + PageTitle.title "Project Settings" + |> PageTitle.withDescription "Manage your project visibility and settings." + + +viewLoadingPage : PageLayout msg +viewLoadingPage = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle pageTitle + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewPageContent : ProjectDetails -> Model -> PageContent Msg +viewPageContent project model = + let + activeVisiblityValue = + case model.form of + NoChanges -> + project.visibility + + WithChanges { visibility } -> + visibility + + Saving { visibility } -> + visibility + + SaveSuccessful -> + project.visibility + + SaveFailed { visibility } _ -> + visibility + + projectVisibilityField = + RadioField.field "project-visibility" + UpdateVisibility + (NEL.Nonempty + (RadioField.option + "Private" + "Only you can see and download this project." + Private + ) + [ RadioField.option + "Public" + "Everyone on the internet can see and download this project." + Public + ] + ) + activeVisiblityValue + + form = + Card.card + [ div [ class "form" ] + [ h2 [] + [ text "Project Visibility" ] + , RadioField.view projectVisibilityField + ] + ] + |> Card.asContained + |> Card.view + + buttons_ = + { discard = + Button.button DiscardChanges "Discard Changes" + |> Button.medium + |> Button.subdued + , save = + Button.button SaveChanges "Save" + |> Button.medium + |> Button.emphasized + } + + disabledButtons = + { discard = buttons_.discard |> Button.disabled + , save = buttons_.save |> Button.disabled + } + + ( buttons, stateClass, message ) = + case model.form of + NoChanges -> + ( disabledButtons, "no-changes", UI.nothing ) + + WithChanges _ -> + ( buttons_, "with-changes", UI.nothing ) + + Saving _ -> + ( disabledButtons, "saving", StatusBanner.working "Saving project" ) + + SaveSuccessful -> + ( buttons_, "save-success", StatusBanner.good "Successfully saved project!" ) + + SaveFailed _ _ -> + ( buttons_, "save-failed", StatusBanner.bad "Couldn't save project, please try again" ) + in + PageContent.oneColumn + [ div [ class "settings-content", class stateClass ] + [ form + , footer [ class "actions" ] + [ message + , div [ class "buttons" ] + [ buttons.discard |> Button.view + , buttons.save |> Button.view + ] + ] + ] + ] + |> PageContent.withPageTitle + (PageTitle.withRightSide + [ Button.iconThenLabel ShowDeleteProjectModal Icon.trash "Delete Project" + |> Button.small + |> Button.critical + |> Button.view + ] + pageTitle + ) + + +view : Session -> ProjectDetails -> Model -> PageLayout Msg +view session project model = + if Session.hasProjectAccess project.ref session then + PageLayout.centeredNarrowLayout + (viewPageContent project model) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + else + let + ( errorTitle, errorMessage ) = + case session of + Session.Anonymous -> + ( "Not signed in" + , "You must be signed in to view Project Settings." + ) + + Session.SignedIn _ -> + ( "No access" + , "Sorry, but your account does not have access to Project Settings for '" ++ ProjectRef.toString project.ref ++ "'." + ) + in + PageLayout.centeredNarrowLayout + (PageContent.oneColumn + [ ErrorCard.errorCard errorTitle errorMessage + |> ErrorCard.toCard + |> Card.asContainedWithFade + |> Card.view + ] + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground diff --git a/src/UnisonShare/Page/ProjectTicketPage.elm b/src/UnisonShare/Page/ProjectTicketPage.elm new file mode 100644 index 00000000..6a9a6ad7 --- /dev/null +++ b/src/UnisonShare/Page/ProjectTicketPage.elm @@ -0,0 +1,465 @@ +module UnisonShare.Page.ProjectTicketPage exposing (..) + +import Html exposing (Html, div, em, header, span, strong, text) +import Html.Attributes exposing (class) +import Http +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.UserHandle as Userhandle +import Markdown +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.ByAt as ByAt +import UI.Card as Card +import UI.DateTime as DateTime exposing (DateTime) +import UI.Icon as Icon +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle exposing (PageTitle) +import UI.Placeholder as Placeholder +import UnisonShare.Account as Account +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.DateTimeContext exposing (DateTimeContext) +import UnisonShare.Page.ErrorPage as ErrorPage +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.ProjectTicketFormModal as ProjectTicketFormModal +import UnisonShare.Session as Session exposing (Session) +import UnisonShare.Ticket as Ticket exposing (Ticket) +import UnisonShare.Ticket.TicketEvent as TicketEvent +import UnisonShare.Ticket.TicketRef as TicketRef exposing (TicketRef) +import UnisonShare.Ticket.TicketStatus exposing (TicketStatus(..)) +import UnisonShare.Ticket.TicketTimeline as TicketTimeline +import UnisonShare.Timeline.TimelineEvent as TimelineEvent + + + +-- MODEL + + +type UpdateStatus + = TimelineNotReady + | Idle + | UpdatingStatus + | UpdateStatusFailed Http.Error + + +type TicketModal + = NoModal + | EditModal ProjectTicketFormModal.Model + + +type alias Model = + { ticket : WebData Ticket + , timeline : TicketTimeline.Model + , updateStatus : UpdateStatus + , modal : TicketModal + } + + +init : AppContext -> ProjectRef -> TicketRef -> ( Model, Cmd Msg ) +init appContext projectRef ticketRef = + let + ( timeline, timelineCmd ) = + TicketTimeline.init appContext projectRef ticketRef + in + ( { ticket = Loading + , timeline = timeline + , updateStatus = Idle + , modal = NoModal + } + , Cmd.batch + [ fetchTicket appContext projectRef ticketRef + , Cmd.map TicketTimelineMsg timelineCmd + ] + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchTicketFinished (WebData Ticket) + | UpdateStatus TicketStatus + | UpdateStatusFinished TicketStatus (HttpResult ()) + | ShowEditModal + | ProjectTicketFormModalMsg ProjectTicketFormModal.Msg + | CloseModal + | TicketTimelineMsg TicketTimeline.Msg + + +update : AppContext -> ProjectRef -> TicketRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef ticketRef msg model = + case msg of + NoOp -> + ( model, Cmd.none ) + + FetchTicketFinished ticket -> + ( { model | ticket = ticket }, Cmd.none ) + + UpdateStatus newStatus -> + case model.ticket of + Success ticket -> + ( { model | updateStatus = UpdatingStatus } + , updateTicketStatus appContext projectRef ticket.ref newStatus + ) + + _ -> + ( model, Cmd.none ) + + UpdateStatusFinished newStatus res -> + case appContext.session of + Session.SignedIn me -> + case ( res, model.ticket ) of + ( Ok _, Success ticket ) -> + let + ticket_ = + Success { ticket | status = newStatus } + + ticketEvent = + TicketEvent.StatusChange + { newStatus = newStatus + , oldStatus = Just ticket.status + , timestamp = appContext.now + , actor = Account.toUserSummary me + } + in + ( { model + | ticket = ticket_ + , timeline = TicketTimeline.addEvent model.timeline ticketEvent + , updateStatus = Idle + } + , Cmd.none + ) + + ( Err e, Success _ ) -> + ( { model | updateStatus = UpdateStatusFailed e }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + Session.Anonymous -> + ( model, Cmd.none ) + + ShowEditModal -> + case ( appContext.session, model.ticket ) of + ( Session.SignedIn _, Success ticket ) -> + let + formModel = + ProjectTicketFormModal.init (ProjectTicketFormModal.Edit ticket) + in + ( { model | modal = EditModal formModel }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + ProjectTicketFormModalMsg formMsg -> + case ( appContext.session, model.modal ) of + ( Session.SignedIn _, EditModal formModel ) -> + let + ( projectTicketFormModal, cmd, out ) = + ProjectTicketFormModal.update appContext + projectRef + formMsg + formModel + + ( modal, ticket ) = + case out of + ProjectTicketFormModal.None -> + ( EditModal projectTicketFormModal, model.ticket ) + + ProjectTicketFormModal.RequestToCloseModal -> + ( NoModal, model.ticket ) + + ProjectTicketFormModal.Saved c -> + -- TODO: also add a TicketEvent + ( NoModal, Success c ) + in + ( { model | modal = modal, ticket = ticket } + , Cmd.map ProjectTicketFormModalMsg cmd + ) + + _ -> + ( model, Cmd.none ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none ) + + TicketTimelineMsg timelineMsg -> + let + ( timeline, timelineCmd ) = + TicketTimeline.update appContext projectRef ticketRef timelineMsg model.timeline + in + ( { model | timeline = timeline }, Cmd.map TicketTimelineMsg timelineCmd ) + + + +-- EFFECTS + + +fetchTicket : AppContext -> ProjectRef -> TicketRef -> Cmd Msg +fetchTicket appContext projectRef ticketRef = + ShareApi.projectTicket projectRef ticketRef + |> HttpApi.toRequest Ticket.decode (RemoteData.fromResult >> FetchTicketFinished) + |> HttpApi.perform appContext.api + + +updateTicketStatus : + AppContext + -> ProjectRef + -> TicketRef + -> TicketStatus + -> Cmd Msg +updateTicketStatus appContext projectRef ticketRef newStatus = + let + update_ = + ShareApi.ProjectTicketStatusUpdate newStatus + in + ShareApi.updateProjectTicket projectRef ticketRef update_ + |> HttpApi.toRequestWithEmptyResponse (UpdateStatusFinished newStatus) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewTicket : Session -> ProjectRef -> UpdateStatus -> Ticket -> Html Msg +viewTicket session projectRef updateStatus ticket = + let + isContributor = + ticket.authorHandle + |> Maybe.map (\h -> Session.isHandle h session) + |> Maybe.withDefault False + + hasProjectAccess = + Session.hasProjectAccess projectRef session + + className = + if updateStatus == UpdatingStatus then + "ticket-description ticket-description_updating" + + else + "ticket-description" + + description = + Markdown.toHtml [ class "definition-doc" ] + ticket.description + + closeButton = + if (hasProjectAccess || isContributor) && updateStatus /= TimelineNotReady then + Button.iconThenLabel (UpdateStatus Closed) Icon.archive "Close" + |> Button.view + + else + UI.nothing + + reopenButton = + if hasProjectAccess || isContributor then + Button.iconThenLabel (UpdateStatus Open) Icon.conversation "Re-open" + |> Button.outlined + |> Button.view + + else + UI.nothing + + actions = + case ticket.status of + Open -> + [ div [ class "right-actions" ] [ closeButton ] ] + + Closed -> + [ div [ class "right-actions" ] [ reopenButton ] ] + + actions_ = + if List.isEmpty actions then + UI.nothing + + else + div [ class "actions" ] actions + in + Card.card + [ description, actions_ ] + |> Card.asContained + |> Card.withClassName className + |> Card.view + + +viewStatusChangeEvent : DateTimeContext a -> TicketEvent.StatusChangeDetails -> List (Html Msg) +viewStatusChangeEvent dtContext { newStatus, oldStatus, actor, timestamp } = + let + byAt = + ByAt.byAt actor timestamp + |> ByAt.view dtContext.timeZone dtContext.now + in + case newStatus of + Open -> + let + title = + case oldStatus of + Just Closed -> + "Re-opened" + + _ -> + "Opened" + in + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.conversation + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle title ] + , byAt + ] + ] + ] + + Closed -> + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.archive + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle "Closed" ] + , byAt + ] + ] + ] + + +viewPageContent : + AppContext + -> ProjectRef + -> UpdateStatus + -> Ticket + -> TicketTimeline.Model + -> PageContent Msg +viewPageContent appContext projectRef updateStatus ticket timeline = + let + timeline_ = + TicketTimeline.view appContext projectRef timeline + in + PageContent.oneColumn + [ viewTicket appContext.session projectRef updateStatus ticket + , Html.map TicketTimelineMsg timeline_ + ] + |> PageContent.withPageTitle (detailedPageTitle appContext ticket) + + +timeAgo : DateTimeContext a -> DateTime -> Html msg +timeAgo dateTimeContext t = + DateTime.view (DateTime.DistanceFrom dateTimeContext.now) dateTimeContext.timeZone t + + +detailedPageTitle : AppContext -> Ticket -> PageTitle Msg +detailedPageTitle appContext ticket = + let + isContributor = + ticket.authorHandle + |> Maybe.map (\h -> Session.isHandle h appContext.session) + |> Maybe.withDefault False + + rightSide = + if isContributor then + [ Button.iconThenLabel ShowEditModal Icon.writingPad "Edit" + |> Button.small + |> Button.outlined + |> Button.view + ] + + else + [] + + author = + case ticket.authorHandle of + Just h -> + strong [] [ text (Userhandle.toString h) ] + + Nothing -> + em [] [ text "Unknown user" ] + in + PageTitle.title ticket.title + |> PageTitle.withLeftTitleText (TicketRef.toString ticket.ref) + |> PageTitle.withDescription_ + (div [] + [ text "by " + , author + , text " " + , span [ class "time-ago" ] [ timeAgo appContext ticket.createdAt ] + ] + ) + |> PageTitle.withRightSide rightSide + + +viewLoadingPage : PageLayout Msg +viewLoadingPage = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ div [] + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + ] + |> PageContent.withPageTitle (PageTitle.title "Loading") + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewErrorPage : Session -> TicketRef -> Http.Error -> PageLayout Msg +viewErrorPage session _ error = + ErrorPage.view session error "ticket" "project-ticket" + + +view : AppContext -> ProjectRef -> TicketRef -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext projectRef ticketRef model = + case model.ticket of + NotAsked -> + ( viewLoadingPage, Nothing ) + + Loading -> + ( viewLoadingPage, Nothing ) + + Success ticket -> + let + modal = + case model.modal of + -- TODO + EditModal form -> + Just + (Html.map ProjectTicketFormModalMsg + (ProjectTicketFormModal.view "Save ticket" form) + ) + + _ -> + Nothing + + pageContent = + viewPageContent + appContext + projectRef + model.updateStatus + ticket + model.timeline + in + ( PageLayout.centeredNarrowLayout + pageContent + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Failure e -> + ( viewErrorPage appContext.session ticketRef e, Nothing ) diff --git a/src/UnisonShare/Page/ProjectTicketsPage.elm b/src/UnisonShare/Page/ProjectTicketsPage.elm new file mode 100644 index 00000000..a38d9d3c --- /dev/null +++ b/src/UnisonShare/Page/ProjectTicketsPage.elm @@ -0,0 +1,323 @@ +module UnisonShare.Page.ProjectTicketsPage exposing (..) + +import Html exposing (Html, div, h2, header, span, text) +import Html.Attributes exposing (class) +import Json.Decode as Decode +import Lib.HttpApi as HttpApi +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.Button as Button +import UI.ByAt as ByAt +import UI.Card as Card +import UI.Click as Click +import UI.Divider as Divider +import UI.EmptyState as EmptyState +import UI.EmptyStateCard as EmptyStateCard +import UI.Icon as Icon +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle +import UI.Placeholder as Placeholder +import UI.TabList as TabList +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Link as Link +import UnisonShare.Page.ErrorPage as ErrorPage +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project as Project exposing (Project) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.ProjectTicketFormModal as ProjectTicketFormModal +import UnisonShare.Session as Session exposing (Session) +import UnisonShare.Ticket as Ticket exposing (Ticket) +import UnisonShare.Ticket.TicketRef as TicketRef +import UnisonShare.Ticket.TicketStatus as TicketStatus + + + +-- MODEL + + +type ContribitionsModal + = NoModal + | SubmitTicketModal ProjectTicketFormModal.Model + + +type Tab + = Open + | Closed + + +type alias Model = + { tickets : WebData (List Ticket) + , modal : ContribitionsModal + , tab : Tab + } + + +init : AppContext -> ProjectRef -> ( Model, Cmd Msg ) +init appContext projectRef = + ( { tickets = Loading + , modal = NoModal + , tab = Open + } + , fetchProjectTickets appContext projectRef + ) + + + +-- UPDATE + + +type Msg + = FetchTicketsFinished (WebData (List Ticket)) + | ShowSubmitTicketModal + | ProjectTicketFormModalMsg ProjectTicketFormModal.Msg + | CloseModal + | ChangeTab Tab + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef msg model = + case msg of + FetchTicketsFinished tickets -> + ( { model | tickets = tickets }, Cmd.none ) + + ShowSubmitTicketModal -> + case appContext.session of + Session.SignedIn _ -> + let + projectTicketFormModal = + ProjectTicketFormModal.init + ProjectTicketFormModal.Create + in + ( { model | modal = SubmitTicketModal projectTicketFormModal } + , Cmd.none + ) + + Session.Anonymous -> + ( model, Cmd.none ) + + ProjectTicketFormModalMsg formMsg -> + case ( appContext.session, model.modal ) of + ( Session.SignedIn _, SubmitTicketModal formModel ) -> + let + ( projectTicketFormModal, cmd, out ) = + ProjectTicketFormModal.update appContext projectRef formMsg formModel + + ( modal, tickets ) = + case out of + ProjectTicketFormModal.None -> + ( SubmitTicketModal projectTicketFormModal, model.tickets ) + + ProjectTicketFormModal.RequestToCloseModal -> + ( NoModal, model.tickets ) + + ProjectTicketFormModal.Saved c -> + ( NoModal, RemoteData.map (\cs -> c :: cs) model.tickets ) + in + ( { model | modal = modal, tickets = tickets } + , Cmd.map ProjectTicketFormModalMsg cmd + ) + + _ -> + ( model, Cmd.none ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none ) + + ChangeTab t -> + ( { model | tab = t }, Cmd.none ) + + + +-- EFFECTS + + +fetchProjectTickets : AppContext -> ProjectRef -> Cmd Msg +fetchProjectTickets appContext projectRef = + ShareApi.projectTickets projectRef + |> HttpApi.toRequest + (Decode.field "items" (Decode.list Ticket.decode)) + (RemoteData.fromResult >> FetchTicketsFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewPageTitle : Session -> Project a -> PageTitle.PageTitle Msg +viewPageTitle session project = + let + pt = + PageTitle.title "Tickets" + + canSubmit = + Session.hasProjectAccess project.ref session || (Session.isSignedIn session && Project.isPublic project) + in + if canSubmit then + pt + |> PageTitle.withRightSide + [ Button.iconThenLabel ShowSubmitTicketModal Icon.merge "New ticket" + |> Button.emphasized + |> Button.view + ] + + else + pt + + +viewLoadingPage : PageLayout msg +viewLoadingPage = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (PageTitle.title "Tickets") + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewTicketRow : AppContext -> ProjectRef -> Ticket -> Html Msg +viewTicketRow appContext projectRef ticket = + let + byAt = + case ticket.authorHandle of + Just h -> + ByAt.handleOnly h ticket.createdAt + + Nothing -> + ByAt.byUnknown ticket.createdAt + + numComments = + if ticket.numComments > 0 then + div [ class "num-comments" ] + [ Icon.view Icon.conversation + , text (String.fromInt ticket.numComments) + ] + + else + UI.nothing + in + div [ class "ticket-row" ] + [ header [ class "ticket-row_header" ] + [ Click.view [] + [ h2 [] + [ span [ class "ticket-row_ref" ] + [ text (TicketRef.toString ticket.ref) + ] + , text ticket.title + ] + ] + (Link.projectTicket projectRef ticket.ref) + , numComments + ] + , div [ class "ticket-row_info" ] + [ ByAt.view appContext.timeZone appContext.now byAt + ] + ] + + +viewPageContent : AppContext -> Project a -> Tab -> List Ticket -> PageContent Msg +viewPageContent appContext project tab tickets = + let + viewEmptyState icon text_ = + EmptyState.iconCloud + (EmptyState.CircleCenterPiece + (div [ class "tickets-empty-state_icon" ] [ Icon.view icon ]) + ) + |> EmptyState.withContent [ h2 [] [ text text_ ] ] + |> EmptyStateCard.view + + ( tabList, status, emptyState ) = + case tab of + Open -> + ( TabList.tabList [] + (TabList.tab "Open" (Click.onClick (ChangeTab Open))) + [ TabList.tab "Closed" (Click.onClick (ChangeTab Closed)) ] + , TicketStatus.Open + , viewEmptyState Icon.conversation "There are currently no open tickets." + ) + + Closed -> + ( TabList.tabList + [ TabList.tab "Open" (Click.onClick (ChangeTab Open)) ] + (TabList.tab "Closed" (Click.onClick (ChangeTab Closed))) + [] + , TicketStatus.Closed + , viewEmptyState Icon.merge "There are currently no merged tickets." + ) + + divider = + Divider.divider + |> Divider.small + |> Divider.withoutMargin + + content = + tickets + |> List.filter (\c -> c.status == status) + |> List.map (viewTicketRow appContext project.ref) + |> List.intersperse (Divider.view divider) + + card = + if List.isEmpty content then + emptyState + + else + Card.card content + |> Card.withClassName "project-tickets" + |> Card.asContained + |> Card.view + in + PageContent.oneColumn [ TabList.view tabList, card ] + |> PageContent.withPageTitle (viewPageTitle appContext.session project) + + +view : AppContext -> Project a -> Model -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext project model = + case model.tickets of + NotAsked -> + ( viewLoadingPage, Nothing ) + + Loading -> + ( viewLoadingPage, Nothing ) + + Success tickets -> + let + modal = + case model.modal of + SubmitTicketModal form -> + Just + (Html.map ProjectTicketFormModalMsg + (ProjectTicketFormModal.view "New ticket" form) + ) + + _ -> + Nothing + in + ( PageLayout.centeredNarrowLayout + (viewPageContent appContext project model.tab tickets) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , modal + ) + + Failure e -> + ( ErrorPage.view appContext.session e "tickets" "project-tickets" + , Nothing + ) diff --git a/src/UnisonShare/Page/TermsOfServicePage.elm b/src/UnisonShare/Page/TermsOfServicePage.elm new file mode 100644 index 00000000..336042c1 --- /dev/null +++ b/src/UnisonShare/Page/TermsOfServicePage.elm @@ -0,0 +1,41 @@ +module UnisonShare.Page.TermsOfServicePage exposing (..) + +import Html exposing (div) +import Html.Attributes exposing (class) +import Markdown +import UI.Card as Card +import UI.Icon as Icon +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.PageTitle as PageTitle +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.PageFooter as PageFooter + + +view : AppDocument msg +view = + let + content = + Card.card [ div [ class "definition-doc" ] [ Markdown.toHtml [] "require:src/terms-of-service.md" ] ] + |> Card.asContained + |> Card.view + + page = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ content ] + |> PageContent.withPageTitle + (PageTitle.title "Terms of Service" + |> PageTitle.withIcon Icon.documentCertificate + ) + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + in + { pageId = "terms-of-service" + , title = "Terms of Service" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Nothing + , page = PageLayout.view page + , modal = Nothing + } diff --git a/src/UnisonShare/Page/UcmConnectedPage.elm b/src/UnisonShare/Page/UcmConnectedPage.elm new file mode 100644 index 00000000..b297381f --- /dev/null +++ b/src/UnisonShare/Page/UcmConnectedPage.elm @@ -0,0 +1,47 @@ +module UnisonShare.Page.UcmConnectedPage exposing (..) + +import Html exposing (br, footer, h1, p, text) +import Html.Attributes exposing (class) +import UI.Button as Button +import UI.Icon as Icon +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.StatusIndicator as StatusIndicator +import UnisonShare.Account exposing (AccountSummary) +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter + + +view : AccountSummary -> AppDocument msg +view account = + let + content = + [ StatusIndicator.good |> StatusIndicator.large |> StatusIndicator.view + , h1 [] [ text "Sweet! You’ve successfully connected UCM and Unison Share 🎉" ] + , p + [] + [ text "Close this window to get back to your UCM session," + , br [] [] + , text "or feel free to explore Unison Share." + ] + , footer [ class "actions" ] + [ Button.iconThenLabel_ Link.catalog Icon.window "Project Catalog" |> Button.medium |> Button.view + , Button.iconThenLabel_ (Link.userCodeRoot account.handle) Icon.chest "Your Codebase" |> Button.medium |> Button.view + , Button.iconThenLabel_ Link.unisonShareDocs Icon.graduationCap "Code Hosting Guide" |> Button.medium |> Button.view + ] + ] + + page = + PageLayout.centeredLayout + (PageContent.oneColumn content) + PageFooter.pageFooter + in + { pageId = "ucm-connected" + , title = "UCM Connected" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Nothing + , page = PageLayout.view page + , modal = Nothing + } diff --git a/src/UnisonShare/Page/UserContributionsPage.elm b/src/UnisonShare/Page/UserContributionsPage.elm new file mode 100644 index 00000000..c8fb3e0e --- /dev/null +++ b/src/UnisonShare/Page/UserContributionsPage.elm @@ -0,0 +1,456 @@ +module UnisonShare.Page.UserContributionsPage exposing (..) + +import Code.Branch as Branch +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.Hash as Hash exposing (Hash) +import Dict +import Html exposing (Html, br, div, h2, p, text) +import Html.Attributes exposing (class) +import Http +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.SearchResults as SearchResults exposing (SearchResults) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Maybe.Extra as MaybeE +import RemoteData exposing (WebData) +import UI +import UI.Card as Card +import UI.EmptyState as EmptyState +import UI.EmptyStateCard as EmptyStateCard +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.KpiTag as KpiTag +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.PageTitle as PageTitle +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UI.StatusIndicator as StatusIndicator +import UI.Tag as Tag +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project as Project exposing (Project) +import UnisonShare.Project.ProjectListing as ProjectListing +import UnisonShare.Project.ProjectRef as ProjectRef + + + +-- MODEL + + +type alias ContributionProject = + Project {} + + +type alias BranchSummary = + Branch.BranchSummary ContributionProject + + +type alias ProjectContributions = + ( ContributionProject, List Contribution ) + + +type alias Contribution = + { branchRef : BranchRef + , project : ContributionProject + , hash : Hash + } + + +type ContributionsSearch + = NotAsked String + | Searching String (Maybe (SearchResults ProjectContributions)) + | Success String (SearchResults ProjectContributions) + | Failure String Http.Error + + +type alias Model = + { contributionsByProject : WebData (List ProjectContributions) + , search : ContributionsSearch + } + + +init : AppContext -> UserHandle -> ( Model, Cmd Msg ) +init appContext handle = + let + params = + { searchQuery = Nothing + , projectRef = Nothing + , limit = 100 + , cursor = Nothing + } + + model = + { contributionsByProject = RemoteData.Loading + , search = NotAsked "" + } + in + ( model, fetchContributions FetchContributionsFinished appContext handle params ) + + + +-- UPDATE + + +type Msg + = FetchContributionsFinished (HttpResult (List Contribution)) + | UpdateSearchQuery String + | FetchSearchResultsFinished String (HttpResult (List Contribution)) + | ClearSearch + + +update : AppContext -> UserHandle -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext handle msg model = + case msg of + FetchContributionsFinished contribs -> + ( { model + | contributionsByProject = + contribs + |> RemoteData.fromResult + |> RemoteData.map toContributionsByProject + } + , Cmd.none + ) + + UpdateSearchQuery query -> + if String.length query <= 2 then + ( { model | search = NotAsked query }, Cmd.none ) + + else + let + previousResults = + case model.search of + Searching _ results -> + results + + Success _ results -> + Just results + + _ -> + Nothing + + cmd = + let + params = + { searchQuery = Just query + , projectRef = Nothing + , limit = 100 + , cursor = Nothing + } + in + fetchContributions (FetchSearchResultsFinished query) appContext handle params + in + ( { model | search = Searching query previousResults }, cmd ) + + ClearSearch -> + ( { model | search = NotAsked "" }, Cmd.none ) + + FetchSearchResultsFinished query result -> + case model.search of + Searching q _ -> + if q == query then + let + search = + case result of + Ok contribs -> + contribs + |> toContributionsByProject + |> SearchResults.fromList + |> Success query + + Err err -> + Failure query err + in + ( { model | search = search }, Cmd.none ) + + else + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + + +-- HELPERS + + +toContributionsByProject : List Contribution -> List ProjectContributions +toContributionsByProject contribs = + let + group contrib acc = + let + key = + ProjectRef.toString contrib.project.ref + + update_ prev = + case prev of + Nothing -> + Just ( contrib.project, [ contrib ] ) + + Just ( pRef, cs ) -> + Just ( pRef, cs ++ [ contrib ] ) + in + Dict.update key update_ acc + in + contribs + |> List.foldl group Dict.empty + |> Dict.values + + + +-- EFFECTS + + +fetchContributions : (HttpResult (List Contribution) -> Msg) -> AppContext -> UserHandle -> ShareApi.UserBranchesParams -> Cmd Msg +fetchContributions toMsg appContext handle params = + let + branchSummariesToContributions : List BranchSummary -> List Contribution + branchSummariesToContributions bs = + List.map + (\b -> + { branchRef = b.ref + , project = b.project + , hash = b.causalHash + } + ) + bs + in + ShareApi.userBranches handle params + |> HttpApi.toRequest (Decode.field "items" (Decode.list (Branch.decodeSummary Project.decode))) + (Result.map branchSummariesToContributions >> toMsg) + |> HttpApi.perform appContext.api + + +viewContribution : Contribution -> Html msg +viewContribution contribution = + let + branchTag = + BranchRef.toTag contribution.branchRef + |> Tag.withClick + (Link.projectBranchRoot + contribution.project.ref + contribution.branchRef + ) + |> Tag.large + in + div [ class "contribution" ] + [ Tag.view branchTag + , Hash.view contribution.hash + ] + + +viewContributionsForProject : ( ContributionProject, List Contribution ) -> Html msg +viewContributionsForProject ( project, contributions ) = + div [ class "contributions-for-project" ] + [ ProjectListing.projectListing project + |> ProjectListing.withClick + Link.userProfile + Link.projectOverview + |> ProjectListing.view + , div [ class "contribution-list" ] (List.map viewContribution contributions) + ] + + +viewContentCard : List (Html msg) -> Html msg +viewContentCard content = + Card.card content + |> Card.asContained + |> Card.withClassName "project-contributions" + |> Card.view + + +viewSubHeaderLoading : Html msg +viewSubHeaderLoading = + Placeholder.text + |> Placeholder.subdued + |> Placeholder.withLength Placeholder.Large + |> Placeholder.view + + +viewLoading : List (Html msg) +viewLoading = + let + project = + Placeholder.text + |> Placeholder.tiny + |> Placeholder.withLength Placeholder.Large + |> Placeholder.view + + contrib len = + Placeholder.text + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.withLength len + |> Placeholder.view + in + [ viewSubHeader viewSubHeaderLoading viewSubHeaderLoading + , viewContentCard + [ div [ class "project-contributions_loading" ] + [ div [ class "project-contributions_loading_project" ] + [ project + , div [ class "project-contributions_loading_contributions" ] + [ contrib Placeholder.Small + , contrib Placeholder.Tiny + , contrib Placeholder.Large + , contrib Placeholder.Medium + ] + ] + , div [ class "project-contributions_loading_project" ] + [ project + , div [ class "project-contributions_loading_contributions" ] + [ contrib Placeholder.Medium + , contrib Placeholder.Small + , contrib Placeholder.Huge + , contrib Placeholder.Large + ] + ] + ] + ] + ] + + +viewLoadingPage : PageLayout.PageLayout msg +viewLoadingPage = + PageLayout.centeredNarrowLayout + (PageContent.oneColumn viewLoading) + PageFooter.pageFooter + + +viewEmptyState : UserHandle -> List (Html Msg) +viewEmptyState handle = + let + emptyState = + EmptyState.iconCloud (EmptyState.CircleCenterPiece (text "🐣")) + |> EmptyState.withContent + [ p + [ class "no-contribs" ] + [ text (UserHandle.toString handle ++ " hasn't created any contributions yet.") ] + , p [] [ text "Check back later." ] + ] + in + [ viewSubHeader + (div [ class "disabled-search-field" ] [ viewSearchField "" (TextField.TextFieldIcon Icon.search) ]) + UI.nothing + , EmptyStateCard.view emptyState + ] + + +viewEmptySearchResults : String -> List (Html Msg) +viewEmptySearchResults query = + [ viewSubHeader (viewSearchField query (TextField.TextFieldIcon Icon.search)) UI.nothing + , viewContentCard + [ EmptyState.search + |> EmptyState.withContent + [ h2 [] [ text "No matches" ] + , p [] + [ text "We looked everywhere, but couldn't find any" + , br [] [] + , text ("contributions matching \"" ++ query ++ "\".") + ] + ] + |> EmptyState.view + ] + ] + + +viewKpis : List ProjectContributions -> Html msg +viewKpis contributionsByProject = + let + numProjects = + List.length contributionsByProject + + numContributions = + contributionsByProject + |> List.foldl (\( _, cs ) acc -> acc ++ cs) [] + |> List.length + in + div [ class "user-contributions-page_kpis" ] + [ KpiTag.kpiTag "Contribution" numContributions + |> KpiTag.withIcon Icon.branch + |> KpiTag.view + , KpiTag.kpiTag "Project" numProjects + |> KpiTag.withIcon Icon.pencilRuler + |> KpiTag.view + ] + + +viewSearchField : String -> TextField.TextFieldIcon Msg -> Html Msg +viewSearchField value textFieldIcon = + TextField.fieldWithoutLabel UpdateSearchQuery "Search branches" value + |> TextField.withTextFieldIcon textFieldIcon + |> TextField.withHelpText "E.g. \"main\", \"feature\"." + |> TextField.withClear ClearSearch + |> TextField.view + + +viewSubHeader : Html msg -> Html msg -> Html msg +viewSubHeader left right = + div [ class "user-contributions-page_sub-header" ] [ left, right ] + + +view : UserHandle -> Model -> PageLayout.PageLayout Msg +view handle { contributionsByProject, search } = + let + content = + case ( search, contributionsByProject ) of + ( NotAsked q, RemoteData.Success contribsByProject ) -> + if List.isEmpty contribsByProject then + viewEmptyState handle + + else + [ viewSubHeader + (viewSearchField q (TextField.TextFieldIcon Icon.search)) + (viewKpis contribsByProject) + , viewContentCard (List.map viewContributionsForProject contribsByProject) + ] + + ( Searching q results, RemoteData.Success contribsByProject ) -> + let + contribsByProject_ = + MaybeE.unwrap contribsByProject SearchResults.toList results + in + [ viewSubHeader + (viewSearchField q (TextField.TextFieldStatusIndicator StatusIndicator.working)) + viewSubHeaderLoading + , viewContentCard + (List.map viewContributionsForProject contribsByProject_ + ++ [ div [ class "user-contributions-page_searching-overlay" ] [] ] + ) + ] + + ( Success q results, _ ) -> + if SearchResults.isEmpty results then + viewEmptySearchResults q + + else + [ viewSubHeader + (viewSearchField q (TextField.TextFieldIcon Icon.search)) + (viewKpis (SearchResults.toList results)) + , viewContentCard + (results + |> SearchResults.toList + |> List.map viewContributionsForProject + ) + ] + + ( Failure q _, _ ) -> + [ viewSubHeader (viewSearchField q (TextField.TextFieldStatusIndicator StatusIndicator.bad)) UI.nothing + , viewContentCard [ StatusBanner.bad "Something broke on our end and we couldn't perform the search. Please try again." ] + ] + + ( NotAsked q, RemoteData.Failure _ ) -> + [ viewSubHeader (viewSearchField q (TextField.TextFieldStatusIndicator StatusIndicator.bad)) UI.nothing + , viewContentCard [ StatusBanner.bad "Something broke on our end and we couldn't perform the search. Please try again." ] + ] + + _ -> + viewLoading + in + PageLayout.centeredNarrowLayout + (PageContent.oneColumn content + |> PageContent.withPageTitle (PageTitle.title "Contributions") + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground diff --git a/src/UnisonShare/Page/UserPage.elm b/src/UnisonShare/Page/UserPage.elm new file mode 100644 index 00000000..48f91fdd --- /dev/null +++ b/src/UnisonShare/Page/UserPage.elm @@ -0,0 +1,512 @@ +module UnisonShare.Page.UserPage exposing (..) + +import Html exposing (h2, text) +import Http +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import RemoteData exposing (RemoteData(..), WebData) +import Tuple +import UI.EmptyState as EmptyState +import UI.EmptyStateCard as EmptyStateCard +import UI.Icon as Icon +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.Sidebar as Sidebar +import UI.StatusMessage as StatusMessage +import UI.ViewMode exposing (ViewMode) +import UnisonShare.Account exposing (AccountSummary) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.AppDocument exposing (AppDocument) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.CodeBrowsingContext as CodeBrowsingContext +import UnisonShare.CodebaseStatus as CodebaseStatus exposing (CodebaseStatus) +import UnisonShare.Page.CodePage as CodePage +import UnisonShare.Page.UserContributionsPage as UserContributionsPage +import UnisonShare.Page.UserProfilePage as UserProfilePage +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Route as Route exposing (CodeRoute, UserRoute(..)) +import UnisonShare.Session as Session +import UnisonShare.SetupInstructions as SetupInstructions +import UnisonShare.User as User exposing (UserDetails) +import UnisonShare.UserPageHeader as UserPageHeader + + + +-- MODEL + + +type SubPage + = Profile UserProfilePage.Model + | Code ViewMode CodePage.Model + | Contributions UserContributionsPage.Model + + +type alias Model = + { user : WebData UserDetails + , subPage : SubPage + , codebaseStatus : WebData CodebaseStatus + , setupInstructions : Maybe ( SetupInstructions.Model, AccountSummary ) + , mobileNavIsOpen : Bool + } + + +init : AppContext -> UserHandle -> UserRoute -> ( Model, Cmd Msg ) +init appContext handle userRoute = + let + codeBrowsingContext = + CodeBrowsingContext.UserCode handle + + ( subPage, cmd ) = + case userRoute of + UserProfile -> + let + ( profilePage, profileCmd ) = + UserProfilePage.init appContext handle + in + ( Profile profilePage, Cmd.map UserProfilePageMsg profileCmd ) + + UserCode vm codeRoute -> + let + ( codePage, codePageCmd ) = + CodePage.init appContext codeBrowsingContext codeRoute + in + ( Code vm codePage, Cmd.map CodePageMsg codePageCmd ) + + UserContributions -> + let + ( contributionsPage, contributionsCmd ) = + UserContributionsPage.init appContext handle + in + ( Contributions contributionsPage, Cmd.map UserContributionsPageMsg contributionsCmd ) + + setupInstructions = + case appContext.session of + Session.SignedIn a -> + if UserHandle.equals handle a.handle then + Just ( SetupInstructions.init appContext a, a ) + + else + Nothing + + _ -> + Nothing + in + ( { user = Loading + , codebaseStatus = Loading + , subPage = subPage + , setupInstructions = Maybe.map (\( ( c, _ ), a ) -> ( c, a )) setupInstructions + , mobileNavIsOpen = False + } + , Cmd.batch + [ fetchUser appContext handle + , CodebaseStatus.checkStatus CodebaseStatusCheckFinished appContext codeBrowsingContext + , cmd + , setupInstructions + |> Maybe.map (Tuple.first >> Tuple.second) + |> Maybe.map (Cmd.map SetupInstructionsMsg) + |> Maybe.withDefault Cmd.none + ] + ) + + + +-- UPDATE + + +type Msg + = FetchUserFinished (WebData UserDetails) + | CodebaseStatusCheckFinished (HttpResult CodebaseStatus) + | ToggleMobileNav + | SetupInstructionsMsg SetupInstructions.Msg + | UserProfilePageMsg UserProfilePage.Msg + | CodePageMsg CodePage.Msg + | UserContributionsPageMsg UserContributionsPage.Msg + + +update : AppContext -> UserHandle -> UserRoute -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext handle route msg model = + case ( model.subPage, msg ) of + ( _, FetchUserFinished u ) -> + ( { model | user = u }, Cmd.none ) + + ( _, CodebaseStatusCheckFinished r ) -> + ( { model | codebaseStatus = RemoteData.fromResult r }, Cmd.none ) + + ( Code viewMode _, SetupInstructionsMsg csiMsg ) -> + case ( model.setupInstructions, route ) of + ( Just ( csi, a ), Route.UserCode _ cr ) -> + let + ( csi_, csiCmd, csiOut ) = + SetupInstructions.update appContext a csiMsg csi + + ( newModel, outCmd ) = + case csiOut of + SetupInstructions.Remain -> + ( { model | setupInstructions = Just ( csi_, a ) }, Cmd.none ) + + SetupInstructions.NoLongerEmpty -> + let + ( codePage, codePageCmd ) = + CodePage.init appContext (CodeBrowsingContext.UserCode handle) cr + in + ( { model + | setupInstructions = Just ( csi_, a ) + , codebaseStatus = Success CodebaseStatus.NotEmpty + , subPage = Code viewMode codePage + } + , Cmd.map CodePageMsg codePageCmd + ) + + SetupInstructions.Exit -> + ( { model | setupInstructions = Nothing }, Cmd.none ) + in + ( newModel, Cmd.batch [ Cmd.map SetupInstructionsMsg csiCmd, outCmd ] ) + + _ -> + ( model, Cmd.none ) + + ( _, ToggleMobileNav ) -> + ( { model | mobileNavIsOpen = not model.mobileNavIsOpen }, Cmd.none ) + + -- Sub msgs + ( Profile profilePage, UserProfilePageMsg userProfilePageMsg ) -> + let + ( profilePage_, profileCmd, out ) = + UserProfilePage.update appContext handle model.user userProfilePageMsg profilePage + + user = + case out of + UserProfilePage.NoOut -> + model.user + + UserProfilePage.UpdateUserProfile u -> + RemoteData.map (\_ -> u) model.user + in + ( { model | subPage = Profile profilePage_, user = user }, Cmd.map UserProfilePageMsg profileCmd ) + + ( Code viewMode codePage, CodePageMsg codePageMsg ) -> + let + codeBrowsingContext = + CodeBrowsingContext.UserCode handle + in + case route of + Route.UserCode _ cr -> + let + ( codePage_, codePageCmd ) = + CodePage.update appContext codeBrowsingContext viewMode cr codePageMsg codePage + in + ( { model | subPage = Code viewMode codePage_ } + , Cmd.map CodePageMsg codePageCmd + ) + + _ -> + ( model, Cmd.none ) + + ( Contributions contributionsPage, UserContributionsPageMsg userContributionsPageMsg ) -> + let + ( contributionsPage_, contributionsCmd ) = + UserContributionsPage.update appContext handle userContributionsPageMsg contributionsPage + in + ( { model | subPage = Contributions contributionsPage_ }, Cmd.map UserContributionsPageMsg contributionsCmd ) + + _ -> + ( model, Cmd.none ) + + +{-| Pass through to CodePage. Used by App when routes change +-} +updateSubPage : AppContext -> UserHandle -> Model -> UserRoute -> ( Model, Cmd Msg ) +updateSubPage appContext handle model route = + let + codeBrowsingContext = + CodeBrowsingContext.UserCode handle + in + case route of + UserProfile -> + case model.subPage of + Profile _ -> + ( model, Cmd.none ) + + _ -> + let + ( profilePage, profileCmd ) = + UserProfilePage.init appContext handle + in + ( { model | subPage = Profile profilePage }, Cmd.map UserProfilePageMsg profileCmd ) + + UserCode vm codeRoute -> + case model.subPage of + Code _ codeSubPage -> + let + ( codePage, codePageCmd ) = + CodePage.updateSubPage appContext codeBrowsingContext codeRoute codeSubPage + in + ( { model | subPage = Code vm codePage } + , Cmd.map CodePageMsg codePageCmd + ) + + _ -> + let + ( codePage, codePageCmd ) = + CodePage.init appContext codeBrowsingContext codeRoute + in + ( { model | subPage = Code vm codePage }, Cmd.map CodePageMsg codePageCmd ) + + UserContributions -> + case model.subPage of + Contributions _ -> + ( model, Cmd.none ) + + _ -> + let + ( contributionsPage, contributionsCmd ) = + UserContributionsPage.init appContext handle + in + ( { model | subPage = Contributions contributionsPage }, Cmd.map UserContributionsPageMsg contributionsCmd ) + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions model = + case model.subPage of + Code _ ucp -> + Sub.map CodePageMsg (CodePage.subscriptions ucp) + + _ -> + Sub.none + + + +-- EFFECTS + + +navigateToCode : AppContext -> UserHandle -> CodeRoute -> Cmd Msg +navigateToCode appContext handle codeRoute = + Route.navigate appContext.navKey (Route.userCode handle codeRoute) + + +fetchUser : AppContext -> UserHandle -> Cmd Msg +fetchUser appContext handle = + ShareApi.user handle + |> HttpApi.toRequest User.decodeDetails (RemoteData.fromResult >> FetchUserFinished) + |> HttpApi.perform appContext.api + + + +-- HELPERS + + +isSignedInUser : AppContext -> Model -> Bool +isSignedInUser appContext model = + case ( appContext.session, model.user ) of + ( Session.SignedIn account, Success user ) -> + UserHandle.equals account.handle user.handle + + _ -> + False + + + +-- VIEW + + +viewErrorPage : AppContext -> SubPage -> UserHandle -> Http.Error -> AppDocument msg +viewErrorPage appContext subPage handle error = + let + page = + case ( error, subPage ) of + ( Http.BadStatus 404, Code _ _ ) -> + PageLayout.sidebarEdgeToEdgeLayout + appContext.operatingSystem + (Sidebar.empty "main-sidebar") + (PageContent.oneColumn [ StatusMessage.bad "Error, page not found" [] |> StatusMessage.view ]) + PageFooter.pageFooter + + ( Http.BadStatus 404, _ ) -> + PageLayout.centeredLayout + (PageContent.oneColumn + [ EmptyState.iconCloud + (EmptyState.IconCenterPiece Icon.profile) + |> EmptyState.withContent [ h2 [] [ text ("Couldn't find user " ++ UserHandle.toString handle) ] ] + |> EmptyStateCard.view + ] + ) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + ( _, Code _ _ ) -> + PageLayout.sidebarEdgeToEdgeLayout + appContext.operatingSystem + (Sidebar.empty "main-sidebar") + (PageContent.oneColumn [ StatusMessage.bad "Error, could not load page" [] |> StatusMessage.view ]) + PageFooter.pageFooter + + _ -> + PageLayout.centeredLayout + (PageContent.oneColumn [ StatusMessage.bad "Error, could not load page" [] |> StatusMessage.view ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + in + { pageId = "user-page user-page-error" + , title = UserHandle.toString handle ++ " | Error" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Just UserPageHeader.error + , page = PageLayout.view page + , modal = Nothing + } + + +viewLoadingPage : AppContext -> SubPage -> UserHandle -> AppDocument msg +viewLoadingPage appContext subPage handle = + let + ( page, pageId ) = + case subPage of + Profile _ -> + ( UserProfilePage.viewLoadingPage, "user-profile-page" ) + + Code _ _ -> + ( PageLayout.sidebarLeftContentLayout + appContext.operatingSystem + (Sidebar.empty "main-sidebar") + (PageContent.oneColumn [ text "" ]) + PageFooter.pageFooter + , "code-page" + ) + + Contributions _ -> + ( UserContributionsPage.viewLoadingPage, "user-contributions-page" ) + in + { pageId = "user-page user-page_loading " ++ pageId + , title = UserHandle.toString handle ++ " | Loading..." + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Just UserPageHeader.loading + , page = PageLayout.view page + , modal = Nothing + } + + +view : AppContext -> UserHandle -> Model -> AppDocument Msg +view appContext handle model = + let + handle_ = + UserHandle.toString handle + + userProfilePageHeader activeNavItem user = + UserPageHeader.view + ToggleMobileNav + model.mobileNavIsOpen + activeNavItem + handle + user + in + case model.user of + NotAsked -> + viewLoadingPage appContext model.subPage handle + + Loading -> + viewLoadingPage appContext model.subPage handle + + Failure e -> + viewErrorPage appContext model.subPage handle e + + Success user -> + case model.subPage of + Profile profilePage -> + let + ( page, modal ) = + UserProfilePage.view appContext.session user profilePage + |> Tuple.mapFirst (PageLayout.map UserProfilePageMsg) + |> Tuple.mapFirst PageLayout.view + |> Tuple.mapSecond (Maybe.map (Html.map UserProfilePageMsg)) + + activeNavItem = + if isSignedInUser appContext model then + AppHeader.Profile + + else + AppHeader.None + in + { pageId = "user-page user-profile-page" + , title = handle_ + , appHeader = AppHeader.appHeader activeNavItem + , pageHeader = Just (userProfilePageHeader UserPageHeader.UserProfile user) + , page = page + , modal = modal + } + + Contributions contributionsPage -> + { pageId = "user-page user-contributions-page" + , title = handle_ ++ " | Contributions" + , appHeader = AppHeader.appHeader AppHeader.None + , pageHeader = Just (userProfilePageHeader UserPageHeader.Contributions user) + , page = + UserContributionsPage.view handle contributionsPage + |> PageLayout.map UserContributionsPageMsg + |> PageLayout.view + , modal = Nothing + } + + Code viewMode_ codeSubPage -> + let + codeBrowsingContext = + CodeBrowsingContext.UserCode handle + + pageTitle = + handle_ ++ " | Code" + + appDoc page viewMode modal = + { pageId = "user-page code-page" + , title = pageTitle + , appHeader = + AppHeader.appHeader AppHeader.None + |> AppHeader.withViewMode viewMode + , pageHeader = Just (userProfilePageHeader UserPageHeader.Code user) + , page = PageLayout.view page + , modal = modal + } + in + case model.codebaseStatus of + Success CodebaseStatus.Empty -> + let + ( codePage, modal_ ) = + CodePage.view appContext + CodePageMsg + viewMode_ + codeBrowsingContext + CodebaseStatus.Empty + codeSubPage + in + case model.setupInstructions of + Just ( csi, a ) -> + appDoc + (codePage + |> PageLayout.withContent + (PageContent.oneColumn [ Html.map SetupInstructionsMsg (SetupInstructions.view appContext a csi) ]) + ) + viewMode_ + modal_ + + Nothing -> + appDoc codePage viewMode_ modal_ + + Success CodebaseStatus.NotEmpty -> + let + ( codePage, modal_ ) = + CodePage.view appContext + CodePageMsg + viewMode_ + codeBrowsingContext + CodebaseStatus.NotEmpty + codeSubPage + in + appDoc codePage viewMode_ modal_ + + Failure e -> + viewErrorPage appContext model.subPage handle e + + _ -> + viewLoadingPage appContext model.subPage handle diff --git a/src/UnisonShare/Page/UserProfilePage.elm b/src/UnisonShare/Page/UserProfilePage.elm new file mode 100644 index 00000000..827771ec --- /dev/null +++ b/src/UnisonShare/Page/UserProfilePage.elm @@ -0,0 +1,359 @@ +module UnisonShare.Page.UserProfilePage exposing (..) + +import Html exposing (Html, div, form, text) +import Html.Attributes exposing (class) +import Http +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Lib.Util exposing (unicodeStringLength) +import Markdown +import Maybe.Extra as MaybeE +import RemoteData exposing (RemoteData(..), WebData) +import Set +import UI +import UI.Button as Button +import UI.Card as Card +import UI.Divider as Divider +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.Modal as Modal +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout +import UI.PageTitle as PageTitle +import UI.ProfileSnippet as ProfileSnippet +import UI.StatusBanner as StatusBanner +import UI.Tag as Tag +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Link as Link +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Project as Project exposing (ProjectSummary) +import UnisonShare.Project.ProjectListing as ProjectListing +import UnisonShare.Session as Session exposing (Session) +import UnisonShare.User exposing (UserDetails) + + + +-- MODEL + + +type alias ProfileFormFields = + { bio : String } + + +type ProfileForm + = Editing ProfileFormFields + | Saving ProfileFormFields + -- Success isn't tracked, we just close the modal. + | SaveFailed ProfileFormFields Http.Error + + +type UserProfileModal + = NoModal + | EditProfileModal ProfileForm + + +type alias Model = + { projects : WebData (List ProjectSummary) + , modal : UserProfileModal + } + + +init : AppContext -> UserHandle -> ( Model, Cmd Msg ) +init appContext handle = + ( { projects = Loading, modal = NoModal } + , fetchProjects appContext handle + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchProjectsFinished (WebData (List ProjectSummary)) + | ShowEditProfileModal + | UpdateBioField String + | SaveProfile + | SaveProfileFinished (HttpResult ()) + | CloseModal + + +type OutMsg + = NoOut + | UpdateUserProfile UserDetails + + +update : AppContext -> UserHandle -> WebData UserDetails -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext handle user msg model = + case msg of + NoOp -> + ( model, Cmd.none, NoOut ) + + FetchProjectsFinished projects -> + ( { model | projects = projects }, Cmd.none, NoOut ) + + ShowEditProfileModal -> + case ( Session.isHandle handle appContext.session, RemoteData.map .bio user ) of + ( True, Success (Just b) ) -> + ( { model | modal = EditProfileModal (Editing { bio = b }) }, Cmd.none, NoOut ) + + ( True, Success Nothing ) -> + ( { model | modal = EditProfileModal (Editing { bio = "" }) }, Cmd.none, NoOut ) + + _ -> + ( model, Cmd.none, NoOut ) + + UpdateBioField b -> + ( { model | modal = EditProfileModal (Editing { bio = b }) }, Cmd.none, NoOut ) + + SaveProfile -> + let + form = + case model.modal of + EditProfileModal (Editing f) -> + Just f + + EditProfileModal (SaveFailed f _) -> + Just f + + _ -> + Nothing + in + case ( Session.handle appContext.session, form ) of + ( Just h, Just f ) -> + ( { model | modal = EditProfileModal (Saving f) }, updateProfile appContext h f, NoOut ) + + _ -> + ( model, Cmd.none, NoOut ) + + SaveProfileFinished result -> + case ( user, model.modal, result ) of + ( Success u, EditProfileModal (Saving f), Ok _ ) -> + ( { model | modal = NoModal } + , Cmd.none + , UpdateUserProfile { u | bio = Just f.bio } + ) + + ( _, EditProfileModal (Saving f), Err e ) -> + ( { model | modal = EditProfileModal (SaveFailed f e) } + , Cmd.none + , NoOut + ) + + _ -> + ( model, Cmd.none, NoOut ) + + CloseModal -> + ( { model | modal = NoModal }, Cmd.none, NoOut ) + + + +-- EFFECTS + + +fetchProjects : AppContext -> UserHandle -> Cmd Msg +fetchProjects appContext handle = + ShareApi.userProjects handle + |> HttpApi.toRequest + (Decode.list Project.decodeSummary) + (RemoteData.fromResult >> FetchProjectsFinished) + |> HttpApi.perform appContext.api + + +updateProfile : AppContext -> UserHandle -> ProfileFormFields -> Cmd Msg +updateProfile appContext handle fields = + ShareApi.updateUserProfile handle fields + |> HttpApi.toRequestWithEmptyResponse SaveProfileFinished + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewEditProfileModal : ProfileForm -> Html Msg +viewEditProfileModal profileForm = + let + form_ f = + form [ class "description-form" ] + [ TextField.field UpdateBioField "Bio" f.bio + |> TextField.withHelpText (String.fromInt (unicodeStringLength f.bio) ++ "/400 characters.") + |> TextField.withRows 7 + |> TextField.withMaxlength 400 + |> TextField.withAutofocus + |> TextField.view + ] + + content_ form__ = + div + [ class "edit-user-profile-modal_content" ] + [ form__ + ] + + { content, showActions, dimOverlay, banner } = + case profileForm of + Editing f -> + { content = content_ (form_ f) + , showActions = True + , dimOverlay = False + , banner = UI.nothing + } + + Saving f -> + { content = content_ (form_ f) + , showActions = True + , dimOverlay = True + , banner = StatusBanner.working "Saving.." + } + + SaveFailed f _ -> + { content = content_ (form_ f) + , showActions = True + , dimOverlay = False + , banner = StatusBanner.bad "Save failed, try again" + } + in + Modal.modal "edit-user-profile-modal" CloseModal (Modal.Content content) + |> Modal.withActionsIf + [ Button.button CloseModal "Cancel" + |> Button.subdued + |> Button.medium + , Button.button SaveProfile "Save" + |> Button.emphasized + |> Button.medium + ] + showActions + |> Modal.withLeftSideFooter [ banner ] + |> Modal.withDimOverlay dimOverlay + |> Modal.withHeaderIf "Edit profile" showActions + |> Modal.view + + +viewProjects : List ProjectSummary -> Html msg +viewProjects projects_ = + case projects_ of + [] -> + UI.nothing + + projects -> + let + viewProject p = + let + tags = + case Set.toList p.tags of + [] -> + UI.nothing + + ts -> + ts |> List.map Tag.tag |> Tag.viewTags + in + div [ class "project" ] + [ p + |> ProjectListing.projectListing + |> ProjectListing.withClick Link.userProfile Link.projectOverview + |> ProjectListing.view + , MaybeE.unwrap UI.nothing (\s -> div [ class "project-summary" ] [ text s ]) p.summary + , tags + ] + + card_ = + Card.titled + "Projects" + [ div [ class "projects" ] + (List.intersperse (Divider.divider |> Divider.small |> Divider.view) + (List.map viewProject projects) + ) + ] + |> Card.asContained + in + Card.view card_ + + +view_ : + Session + -> UserDetails + -> WebData (List ProjectSummary) + -> PageContent Msg +view_ session user projects = + let + pageContent = + [ MaybeE.unwrap + UI.nothing + (\b -> + Card.card [ Markdown.toHtml [] b ] + |> Card.asContained + |> Card.withTitle "BIO" + |> Card.withClassName "bio" + |> Card.view + ) + user.bio + , projects + |> RemoteData.map viewProjects + |> RemoteData.withDefault UI.nothing + ] + + isViewingOwnProfile = + Session.isHandle user.handle session + + titleRightSide = + if isViewingOwnProfile then + [ div [ class "user-profile-empty-state_instructions-banner" ] + [ text (UserHandle.toString user.handle ++ " is you! 😎 This is your profile—your space!") + , Button.iconThenLabel ShowEditProfileModal Icon.writingPad "Edit profile" + |> Button.small + |> Button.view + ] + ] + + else + [] + + pageTitle = + PageTitle.custom + [ ProfileSnippet.profileSnippet + user + |> ProfileSnippet.huge + |> ProfileSnippet.view + ] + |> PageTitle.withRightSide titleRightSide + in + PageContent.oneColumn pageContent + |> PageContent.withPageTitle pageTitle + + +viewLoadingPage : PageLayout.PageLayout msg +viewLoadingPage = + let + content = + PageContent.oneColumn + [ div [ class "user-profile-page_page-content" ] + [ div [ class "user-profile_main-content" ] + [ text "" ] + ] + ] + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +view : Session -> UserDetails -> Model -> ( PageLayout.PageLayout Msg, Maybe (Html Msg) ) +view session user model = + let + page = + PageLayout.centeredNarrowLayout + (view_ session user model.projects) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + modal = + case model.modal of + NoModal -> + Nothing + + EditProfileModal form -> + Just (viewEditProfileModal form) + in + ( page, modal ) diff --git a/src/UnisonShare/PageFooter.elm b/src/UnisonShare/PageFooter.elm new file mode 100644 index 00000000..7ed58a0c --- /dev/null +++ b/src/UnisonShare/PageFooter.elm @@ -0,0 +1,14 @@ +module UnisonShare.PageFooter exposing (..) + +import UI.PageLayout exposing (PageFooter(..)) +import UnisonShare.Link as Link + + +pageFooter : PageFooter msg +pageFooter = + PageFooter + [ Link.view "Status" Link.status + , Link.view "Code of Conduct" Link.codeOfConduct + , Link.view "Terms of Service" Link.termsOfService + , Link.view "Privacy Policy" Link.privacyPolicy + ] diff --git a/src/UnisonShare/PreApp.elm b/src/UnisonShare/PreApp.elm new file mode 100644 index 00000000..acbe98b2 --- /dev/null +++ b/src/UnisonShare/PreApp.elm @@ -0,0 +1,224 @@ +module UnisonShare.PreApp exposing (..) + +import Browser +import Browser.Navigation as Nav +import Html exposing (Html, div, p, text) +import Html.Attributes exposing (class, id, title) +import Http +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import Task exposing (Task) +import Time +import UI.DateTime as DateTime +import UI.Icon as Icon +import UI.PageContent as PageContent +import UI.PageLayout as PageLayout +import UI.ViewMode as ViewMode +import UnisonShare.Api as ShareApi +import UnisonShare.App as App +import UnisonShare.AppContext as AppContext exposing (Flags) +import UnisonShare.AppHeader as AppHeader +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Route as Route exposing (Route) +import UnisonShare.Session as Session exposing (Session) +import Url exposing (Url) + + +type Model + = Initializing PreAppContext + | InitializationError PreAppContext Http.Error + | Initialized App.Model + + +type alias PreAppContext = + { flags : Flags + , route : Route + , currentUrl : Url + , navKey : Nav.Key + } + + +init : Flags -> Url -> Nav.Key -> ( Model, Cmd Msg ) +init flags url navKey = + let + route = + Route.fromUrl flags.basePath url + + preAppContext = + { flags = flags + , route = route + , currentUrl = url + , navKey = navKey + } + in + ( Initializing preAppContext, Task.attempt FetchPreReqsFinished (fetchPreReqs preAppContext) ) + + +type Msg + = AppMsg App.Msg + | FetchPreReqsFinished (HttpResult ( Time.Posix, Time.Zone, Session )) + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case ( model, msg ) of + ( Initializing preAppContext, FetchPreReqsFinished (Ok ( now, timeZone, session )) ) -> + let + appContext = + AppContext.init + preAppContext.flags + preAppContext.navKey + preAppContext.currentUrl + (DateTime.fromPosix now) + timeZone + session + + ( app, cmd ) = + App.init appContext preAppContext.route + in + ( Initialized app, Cmd.map AppMsg cmd ) + + ( Initializing preAppContext, FetchPreReqsFinished (Err e) ) -> + ( InitializationError preAppContext e, Cmd.none ) + + ( _, AppMsg appMsg ) -> + case model of + Initialized a -> + let + ( app, cmd ) = + App.update appMsg a + in + ( Initialized app, Cmd.map AppMsg cmd ) + + _ -> + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + +fetchPreReqs : PreAppContext -> Task Http.Error ( Time.Posix, Time.Zone, Session ) +fetchPreReqs preAppContext = + Task.map3 + (\n z s -> ( n, z, s )) + Time.now + Time.here + (fetchSession preAppContext) + + +fetchSession : PreAppContext -> Task Http.Error Session +fetchSession preAppContext = + let + onError e = + case e of + Http.BadStatus 401 -> + Task.succeed Session.Anonymous + + Http.BadStatus 404 -> + Task.succeed Session.Anonymous + + _ -> + Task.fail e + + apiUrl = + HttpApi.apiUrlFromString True preAppContext.flags.apiUrl + in + HttpApi.toTask apiUrl Session.decode ShareApi.session + |> Task.onError onError + + + +{- + fetchSession : PreAppContext -> Cmd Msg + fetchSession preAppContext = + let + api = + HttpApi.httpApi True preAppContext.flags.apiUrl preAppContext.flags.xsrfToken + in + ShareApi.session + |> HttpApi.toRequest Session.decode FetchSessionFinished + |> HttpApi.perform api +-} + + +subscriptions : Model -> Sub Msg +subscriptions model = + case model of + Initialized app -> + Sub.map AppMsg (App.subscriptions app) + + _ -> + Sub.none + + +viewAppLoading : Html msg +viewAppLoading = + div [ id "app" ] + [ AppHeader.viewBlank ViewMode.Regular + , PageLayout.view + (PageLayout.centeredLayout + PageContent.empty + PageFooter.pageFooter + ) + ] + + +viewAppError : Http.Error -> Html msg +viewAppError error = + let + -- TODO: Better error + ( errorTitle, _ ) = + case error of + Http.Timeout -> + ( "Unison Share took too long to respond.", "" ) + + Http.NetworkError -> + ( "Unison Share is unreachable.", "" ) + + Http.BadUrl _ -> + ( "Unison Share is unavailable.", "AN error occurred on our end." ) + + Http.BadStatus _ -> + ( "Unison Share is unavailable.", "An error occurred on our end." ) + + Http.BadBody _ -> + ( "Unison Share is unavailable.", "An error occurred on our end." ) + in + div [ id "app" ] + [ AppHeader.viewBlank ViewMode.Regular + , PageLayout.view + (PageLayout.centeredLayout + (PageContent.oneColumn + [ div [ class "app-error" ] + [ Icon.view Icon.warn + , p [ title (Util.httpErrorToString error) ] + [ text errorTitle ] + ] + ] + ) + PageFooter.pageFooter + ) + ] + + +view : Model -> Browser.Document Msg +view model = + case model of + Initializing _ -> + { title = "Loading.. | Unison Share" + , body = [ viewAppLoading ] + } + + InitializationError _ error -> + { title = "Application Error | Unison Share" + , body = [ viewAppError error ] + } + + Initialized appModel -> + let + app = + App.view appModel + in + { title = app.title + , body = List.map (Html.map AppMsg) app.body + } diff --git a/src/UnisonShare/Project.elm b/src/UnisonShare/Project.elm new file mode 100644 index 00000000..38aa4f35 --- /dev/null +++ b/src/UnisonShare/Project.elm @@ -0,0 +1,265 @@ +module UnisonShare.Project exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.ProjectName exposing (ProjectName) +import Code.ProjectSlug as ProjectSlug exposing (ProjectSlug) +import Code.Version as Version exposing (Version) +import Json.Decode as Decode exposing (bool, int, nullable, string) +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (optional, required, requiredAt) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Maybe.Extra as MaybeE +import Set exposing (Set) +import UI.DateTime as DateTime exposing (DateTime) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Project.ReleaseDownloads as ReleaseDownloads exposing (ReleaseDownloads) + + +type alias Project a = + { a | ref : ProjectRef, visibility : ProjectVisibility } + + +type IsFaved + = Unknown + | Faved + | NotFaved + | JustFaved + + +type ProjectVisibility + = Public + | Private + + +type alias ProjectSummary = + Project + { summary : Maybe String + , tags : Set String + , numFavs : Int + , isFaved : IsFaved + , createdAt : DateTime + , updatedAt : DateTime + } + + +type alias ProjectDetails = + Project + { summary : Maybe String + , tags : Set String + , numFavs : Int + , numActiveContributions : Int + , numOpenTickets : Int + , releaseDownloads : ReleaseDownloads + , isFaved : IsFaved + , latestVersion : Maybe Version + , defaultBranch : Maybe BranchRef + , createdAt : DateTime + , updatedAt : DateTime + } + + +ref : Project a -> ProjectRef +ref project = + project.ref + + +handle : Project a -> UserHandle +handle p = + ProjectRef.handle p.ref + + +defaultBrowsingBranch : ProjectDetails -> BranchRef +defaultBrowsingBranch p = + p.latestVersion + |> Maybe.map BranchRef.releaseBranchRef + |> MaybeE.orElse p.defaultBranch + |> Maybe.withDefault BranchRef.main_ + + +slug : Project a -> ProjectSlug +slug p = + ProjectRef.slug p.ref + + +projectName : Project a -> ProjectName +projectName p = + ProjectRef.toProjectName p.ref + + +equals : Project a -> Project b -> Bool +equals a b = + ProjectRef.equals a.ref b.ref + + +isOwnedBy : UserHandle -> Project a -> Bool +isOwnedBy handle_ project = + UserHandle.equals handle_ (handle project) + + +isPublic : Project a -> Bool +isPublic project = + project.visibility == Public + + +toggleFav : ProjectDetails -> ProjectDetails +toggleFav ({ numFavs } as project) = + let + ( isFaved_, numFavs_ ) = + case project.isFaved of + Faved -> + ( NotFaved, numFavs - 1 ) + + JustFaved -> + ( NotFaved, numFavs - 1 ) + + NotFaved -> + ( JustFaved, numFavs + 1 ) + + Unknown -> + ( Unknown, numFavs ) + in + { project | isFaved = isFaved_, numFavs = numFavs_ } + + +isFaved : ProjectDetails -> Bool +isFaved p = + isFavedToBool p.isFaved + + +isFavedToBool : IsFaved -> Bool +isFavedToBool isFaved_ = + isFaved_ == Faved || isFaved_ == JustFaved + + +isFavedFromBool : Bool -> IsFaved +isFavedFromBool b = + if b then + Faved + + else + NotFaved + + +visibilityToString : ProjectVisibility -> String +visibilityToString pv = + case pv of + Public -> + "public" + + Private -> + "private" + + +averageWeeklyDownloads : Project { p | releaseDownloads : ReleaseDownloads } -> Int +averageWeeklyDownloads { releaseDownloads } = + ReleaseDownloads.weeklyAverage releaseDownloads + + +fourWeekTotalDownloads : Project { p | releaseDownloads : ReleaseDownloads } -> Int +fourWeekTotalDownloads { releaseDownloads } = + ReleaseDownloads.fourWeekTotal releaseDownloads + + + +-- DECODE + + +decodeVisibility : Decode.Decoder ProjectVisibility +decodeVisibility = + Decode.oneOf + [ when Decode.string ((==) "public") (Decode.succeed Public) + , when Decode.string ((==) "private") (Decode.succeed Private) + ] + + +decodeIsFaved : Decode.Decoder IsFaved +decodeIsFaved = + Decode.map isFavedFromBool bool + + +decodeDetails : Decode.Decoder ProjectDetails +decodeDetails = + let + makeProjectDetails handle_ slug_ summary tags visibility numFavs numActiveContributions numOpenTickets releaseDownloads isFaved_ latestVersion defaultBranch createdAt updatedAt = + let + ref_ = + ProjectRef.projectRef handle_ slug_ + in + { ref = ref_ + , summary = summary + , tags = Set.fromList tags + , visibility = visibility + , numFavs = numFavs + , numActiveContributions = numActiveContributions + , numOpenTickets = numOpenTickets + , releaseDownloads = releaseDownloads + , isFaved = isFaved_ + , latestVersion = latestVersion + , defaultBranch = defaultBranch + , createdAt = createdAt + , updatedAt = updatedAt + } + in + Decode.succeed makeProjectDetails + |> requiredAt [ "owner", "handle" ] UserHandle.decode + |> required "slug" ProjectSlug.decode + |> required "summary" (nullable string) + |> required "tags" (Decode.list string) + |> required "visibility" decodeVisibility + |> optional "numFavs" int 0 + |> optional "numActiveContributions" int 0 + |> optional "numOpenTickets" int 0 + |> required "releaseDownloads" ReleaseDownloads.decode + |> optional "isFaved" decodeIsFaved Unknown + |> required "latestRelease" (nullable Version.decode) + |> required "defaultBranch" (nullable BranchRef.decode) + |> required "createdAt" DateTime.decode + |> required "updatedAt" DateTime.decode + + +decode : Decode.Decoder (Project {}) +decode = + let + makeProject handle_ slug_ visibility = + let + ref_ = + ProjectRef.projectRef handle_ slug_ + in + { ref = ref_ + , visibility = visibility + } + in + Decode.succeed makeProject + |> requiredAt [ "owner", "handle" ] UserHandle.decode + |> required "slug" ProjectSlug.decode + |> required "visibility" decodeVisibility + + +decodeSummary : Decode.Decoder ProjectSummary +decodeSummary = + let + makeProjectSummary handle_ slug_ visibility summary tags numFavs isFaved_ createdAt updatedAt = + let + ref_ = + ProjectRef.projectRef handle_ slug_ + in + { ref = ref_ + , visibility = visibility + , summary = summary + , tags = Set.fromList tags + , numFavs = numFavs + , isFaved = isFaved_ + , createdAt = createdAt + , updatedAt = updatedAt + } + in + Decode.succeed makeProjectSummary + |> requiredAt [ "owner", "handle" ] UserHandle.decode + |> required "slug" ProjectSlug.decode + |> required "visibility" decodeVisibility + |> required "summary" (nullable string) + |> required "tags" (Decode.list string) + |> required "numFavs" int + |> optional "isFaved" decodeIsFaved Unknown + |> required "createdAt" DateTime.decode + |> required "updatedAt" DateTime.decode diff --git a/src/UnisonShare/Project/ProjectDependency.elm b/src/UnisonShare/Project/ProjectDependency.elm new file mode 100644 index 00000000..1de08d1c --- /dev/null +++ b/src/UnisonShare/Project/ProjectDependency.elm @@ -0,0 +1,31 @@ +module UnisonShare.Project.ProjectDependency exposing (ProjectDependency, fromString, toTag) + +import Code.Version as Version exposing (Version) +import Maybe.Extra as MaybeE +import UI.Tag as Tag exposing (Tag) + + +type alias ProjectDependency = + { name : String, version : Maybe Version } + + +fromString : String -> ProjectDependency +fromString nameWithVersion = + let + ( name, version ) = + case String.split "_" nameWithVersion of + n :: v -> + ( n, Version.fromString (String.join "." v) ) + + _ -> + ( nameWithVersion, Nothing ) + in + ProjectDependency name version + + +toTag : ProjectDependency -> Tag msg +toTag { name, version } = + name + |> Tag.tag + |> Tag.large + |> Tag.withRightText (MaybeE.unwrap "" (\v -> "@" ++ Version.toString v) version) diff --git a/src/UnisonShare/Project/ProjectListing.elm b/src/UnisonShare/Project/ProjectListing.elm new file mode 100644 index 00000000..1c5f9878 --- /dev/null +++ b/src/UnisonShare/Project/ProjectListing.elm @@ -0,0 +1,75 @@ +module UnisonShare.Project.ProjectListing exposing (..) + +import Code.ProjectNameListing as ProjectNameListing exposing (ProjectNameListing) +import Html exposing (Html, div) +import Html.Attributes exposing (class) +import Lib.UserHandle exposing (UserHandle) +import UI +import UI.Click exposing (Click) +import UI.Icon as Icon +import UnisonShare.Project as Project exposing (Project) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) + + +type alias ProjectListing p msg = + { project : Project p, listing : ProjectNameListing msg } + + +projectListing : Project p -> ProjectListing p msg +projectListing project = + { project = project + , listing = + ProjectNameListing.projectNameListing + (ProjectRef.toProjectName project.ref) + } + + + +-- MODIFY + + +subdued : ProjectListing p msg -> ProjectListing p msg +subdued pl = + { pl | listing = ProjectNameListing.subdued pl.listing } + + +large : ProjectListing p msg -> ProjectListing p msg +large pl = + { pl | listing = ProjectNameListing.large pl.listing } + + +huge : ProjectListing p msg -> ProjectListing p msg +huge pl = + { pl | listing = ProjectNameListing.huge pl.listing } + + +withClick : (UserHandle -> Click msg) -> (ProjectRef -> Click msg) -> ProjectListing p msg -> ProjectListing p msg +withClick handleClick projectRefClick p = + let + slugClick _ = + projectRefClick p.project.ref + + listing = + p.listing + |> ProjectNameListing.withClick handleClick slugClick + in + { p | listing = listing } + + + +-- VIEW +-- TODO, Add visibility thingy! + + +view : ProjectListing p msg -> Html msg +view pl = + let + visibilityIcon = + case pl.project.visibility of + Project.Public -> + UI.nothing + + Project.Private -> + div [ class "private-icon" ] [ Icon.view Icon.eyeSlash ] + in + div [ class "project-listing" ] [ ProjectNameListing.view pl.listing, visibilityIcon ] diff --git a/src/UnisonShare/Project/ProjectRef.elm b/src/UnisonShare/Project/ProjectRef.elm new file mode 100644 index 00000000..5ab8e738 --- /dev/null +++ b/src/UnisonShare/Project/ProjectRef.elm @@ -0,0 +1,165 @@ +module UnisonShare.Project.ProjectRef exposing + ( ProjectRef + , decode + , equals + , fromString + , handle + , projectRef + , slug + , toApiStringParts + , toParts + , toProjectName + , toString + , toStringParts + , toUrlPath + , unsafeFromString + , view + , viewClickable + , viewHashvatar + , view_ + ) + +import Code.Hash as Hash +import Code.Hashvatar as Hashvatar +import Code.ProjectName as ProjectName exposing (ProjectName) +import Code.ProjectSlug as ProjectSlug exposing (ProjectSlug) +import Html exposing (Html, label, span, text) +import Html.Attributes exposing (class, title) +import Json.Decode as Decode +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Lib.Util as Util +import UI.Click as Click exposing (Click) + + +type ProjectRef + = ProjectRef { handle : UserHandle, slug : ProjectSlug } + + +fromString : String -> String -> Maybe ProjectRef +fromString rawHandle rawSlug = + Maybe.map2 projectRef + (UserHandle.fromString rawHandle) + (ProjectSlug.fromString rawSlug) + + +{-| Don't use! It's meant for tests +-} +unsafeFromString : String -> String -> ProjectRef +unsafeFromString rawHandle rawSlug = + projectRef + (UserHandle.unsafeFromString rawHandle) + (ProjectSlug.unsafeFromString rawSlug) + + +projectRef : UserHandle -> ProjectSlug -> ProjectRef +projectRef handle_ slug_ = + ProjectRef { handle = handle_, slug = slug_ } + + +toString : ProjectRef -> String +toString (ProjectRef p) = + UserHandle.toString p.handle ++ "/" ++ ProjectSlug.toString p.slug + + +toUrlPath : ProjectRef -> List String +toUrlPath (ProjectRef p) = + [ UserHandle.toString p.handle, ProjectSlug.toString p.slug ] + + +toProjectName : ProjectRef -> ProjectName +toProjectName (ProjectRef p) = + ProjectName.projectName (Just p.handle) p.slug + + +handle : ProjectRef -> UserHandle +handle (ProjectRef p) = + p.handle + + +slug : ProjectRef -> ProjectSlug +slug (ProjectRef p) = + p.slug + + +toParts : ProjectRef -> ( UserHandle, ProjectSlug ) +toParts (ProjectRef p) = + ( p.handle, p.slug ) + + +toStringParts : ProjectRef -> ( String, String ) +toStringParts (ProjectRef p) = + ( UserHandle.toString p.handle, ProjectSlug.toString p.slug ) + + +toApiStringParts : ProjectRef -> ( String, String ) +toApiStringParts (ProjectRef p) = + ( UserHandle.toUnprefixedString p.handle, ProjectSlug.toString p.slug ) + + +equals : ProjectRef -> ProjectRef -> Bool +equals a b = + toString a == toString b + + +viewHashvatar : ProjectRef -> Html msg +viewHashvatar ref = + let + hash = + Hash.unsafeFromString (toString ref) + in + Hashvatar.view hash + + +view : ProjectRef -> Html msg +view projectRef_ = + view_ Nothing Nothing projectRef_ + + +viewClickable : (UserHandle -> Click msg) -> (ProjectRef -> Click msg) -> ProjectRef -> Html msg +viewClickable handleClick slugClick projectRef_ = + view_ (Just handleClick) (Just slugClick) projectRef_ + + +view_ : Maybe (UserHandle -> Click msg) -> Maybe (ProjectRef -> Click msg) -> ProjectRef -> Html msg +view_ handleClick slugClick ((ProjectRef p) as projectRef_) = + let + handle_ = + case handleClick of + Just c -> + Click.view [ class "project-ref_handle" ] [ text (UserHandle.toString p.handle) ] (c p.handle) + + Nothing -> + span [ class "project-ref_handle" ] [ text (UserHandle.toString p.handle) ] + + slug_ = + case slugClick of + Just c -> + Click.view [ class "project-ref_slug" ] [ text (ProjectSlug.toString p.slug) ] (c projectRef_) + + Nothing -> + span [ class "project-ref_slug" ] [ text (ProjectSlug.toString p.slug) ] + in + label [ class "project-ref", title (toString projectRef_) ] + [ handle_ + , span [ class "project-ref_separator" ] [ text "/" ] + , slug_ + ] + + + +-- DECODE + + +decode : Decode.Decoder ProjectRef +decode = + let + fromString_ s = + case String.split "/" s of + [ handle_, slug_ ] -> + fromString handle_ slug_ + + _ -> + Nothing + in + Decode.map fromString_ Decode.string + |> Decode.andThen (Util.decodeFailInvalid "Invalid ProjectRef") diff --git a/src/UnisonShare/Project/Release.elm b/src/UnisonShare/Project/Release.elm new file mode 100644 index 00000000..ef83ed13 --- /dev/null +++ b/src/UnisonShare/Project/Release.elm @@ -0,0 +1,130 @@ +module UnisonShare.Project.Release exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.Definition.Doc exposing (Doc) +import Code.Hash as Hash exposing (Hash) +import Code.Version as Version exposing (Version) +import Json.Decode as Decode exposing (field, string) +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (required) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import UI.DateTime as DateTime exposing (DateTime) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) + + +type alias StatusMeta = + { at : DateTime, by : UserHandle } + + +type ReleaseStatus + = Draft + | Published StatusMeta + | Unpublished StatusMeta + + +type alias Release = + { version : Version + , causalHashUnsquashed : Hash + , causalHashSquashed : Hash + , releaseNotes : Maybe Doc + , projectRef : ProjectRef + , createdAt : DateTime + , createdBy : UserHandle + , updatedAt : DateTime + , status : ReleaseStatus + } + + + +-- HELPERS + + +isDraft : Release -> Bool +isDraft r = + case r.status of + Draft -> + True + + _ -> + False + + +isPublished : Release -> Bool +isPublished r = + case r.status of + Published _ -> + True + + _ -> + False + + +isUnpublished : Release -> Bool +isUnpublished r = + case r.status of + Unpublished _ -> + True + + _ -> + False + + +branchRef : Release -> BranchRef +branchRef r = + BranchRef.releaseBranchRef r.version + + + +-- DECODE + + +decode : Decode.Decoder Release +decode = + let + published at by = + Published { at = at, by = by } + + unpublished at by = + Unpublished { at = at, by = by } + + decodeStatus = + Decode.oneOf + [ when (field "status" string) + ((==) "draft") + (Decode.succeed Draft) + , when (field "status" string) + ((==) "published") + (Decode.map2 published + (field "publishedAt" DateTime.decode) + (field "publishedBy" UserHandle.decode) + ) + , when (field "status" string) + ((==) "deprecated") + (Decode.map2 unpublished + (field "deprecatedAt" DateTime.decode) + (field "deprecatedBy" UserHandle.decode) + ) + ] + + release version unsquashed squashed projectRef createdAt createdBy updatedAt status = + { version = version + , causalHashUnsquashed = unsquashed + , causalHashSquashed = squashed + , releaseNotes = Nothing + , projectRef = projectRef + , createdAt = createdAt + , createdBy = createdBy + , updatedAt = updatedAt + , status = status + } + in + Decode.succeed release + |> required "version" Version.decode + |> required "causalHashUnsquashed" Hash.decode + |> required "causalHashSquashed" Hash.decode + -- |> required "releaseNotes" (nullable Doc.decode) + |> required "projectRef" ProjectRef.decode + |> required "createdAt" DateTime.decode + |> required "createdBy" UserHandle.decode + |> required "updatedAt" DateTime.decode + |> required "status" decodeStatus diff --git a/src/UnisonShare/Project/ReleaseDownloads.elm b/src/UnisonShare/Project/ReleaseDownloads.elm new file mode 100644 index 00000000..884ab12d --- /dev/null +++ b/src/UnisonShare/Project/ReleaseDownloads.elm @@ -0,0 +1,36 @@ +module UnisonShare.Project.ReleaseDownloads exposing + ( ReleaseDownloads(..) + , decode + , fourWeekTotal + , weeklyAverage + ) + +import Json.Decode as Decode + + +type ReleaseDownloads + = ReleaseDownloads (List Int) + + +fourWeekTotal : ReleaseDownloads -> Int +fourWeekTotal (ReleaseDownloads downloads) = + List.sum downloads + + +weeklyAverage : ReleaseDownloads -> Int +weeklyAverage (ReleaseDownloads downloads) = + let + avg : List Int -> Int + avg l = + List.sum l // List.length l + in + avg downloads + + + +-- DECODE + + +decode : Decode.Decoder ReleaseDownloads +decode = + Decode.map ReleaseDownloads (Decode.list Decode.int) diff --git a/src/UnisonShare/ProjectContributionFormModal.elm b/src/UnisonShare/ProjectContributionFormModal.elm new file mode 100644 index 00000000..e542fa7e --- /dev/null +++ b/src/UnisonShare/ProjectContributionFormModal.elm @@ -0,0 +1,659 @@ +module UnisonShare.ProjectContributionFormModal exposing + ( FormAction(..) + , Model + , Msg + , OutMsg(..) + , init + , update + , view + ) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Html exposing (Html, aside, div, p, section, span, strong, text) +import Html.Attributes exposing (class) +import Http +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import Maybe.Extra as MaybeE +import RemoteData exposing (RemoteData(..), WebData) +import String.Extra as StringE +import Task exposing (Task) +import UI +import UI.AnchoredOverlay as AnchoredOverlay exposing (AnchoredOverlay) +import UI.Button as Button +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.Modal as Modal +import UI.StatusBanner as StatusBanner +import UI.StatusMessage as StatusMessage +import UnisonShare.Account as Account exposing (AccountSummary) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.BranchSummary as BranchSummary exposing (BranchSummary) +import UnisonShare.Contribution as Contribution exposing (Contribution) +import UnisonShare.Contribution.ContributionRef as ContributionRef exposing (ContributionRef) +import UnisonShare.Contribution.ContributionStatus as ContributionStatus exposing (ContributionStatus) +import UnisonShare.Link as Link +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.SearchBranchSheet as SearchBranchSheet + + + +-- MODEL + + +type alias SelectBranchOpenSheet = + { sheet : SearchBranchSheet.Model } + + +type SelectBranchSheet + = OpenForSource SelectBranchOpenSheet + | OpenForTarget SelectBranchOpenSheet + | Closed + + +type alias RecentBranches = + { ownContributorBranches : List BranchSummary + , projectBranches : List BranchSummary + } + + +type SourceBranchValidity + = Missing + | SameAsTarget + | ValidSourceBranch + + +type Validity + = NotChecked + | Valid { sourceBranchRef : BranchRef } + | Invalid + { needsTitle : Bool + , sourceBranch : SourceBranchValidity + } + + +type alias Form = + { title : String + , description : String + , sourceBranchRef : Maybe BranchRef + , targetBranchRef : BranchRef + , selectBranchSheet : SelectBranchSheet + , save : WebData Contribution + , recentBranches : RecentBranches + , validity : Validity + } + + +type FormAction + = Edit Contribution + | Create + + +type alias Model = + { form : WebData Form + , action : FormAction + } + + +init : AppContext -> AccountSummary -> ProjectRef -> FormAction -> ( Model, Cmd Msg ) +init appContext currentUser projectRef action = + ( { form = Loading, action = action } + , Task.attempt FetchRecentBranchesFinished + (fetchRecentBranches appContext currentUser projectRef) + ) + + + +-- UPDATE + + +type Msg + = CloseModal + | FetchRecentBranchesFinished (HttpResult RecentBranches) + | UpdateSubmissionTitle String + | UpdateSubmissionDescription String + | ToggleSelectSourceBranchSheet + | ToggleSelectTargetBranchSheet + | SearchBranchSheetMsg SearchBranchSheet.Msg + | SaveContribution + | SaveContributionFinished (WebData Contribution) + | SuccessfullySaved Contribution + + +type OutMsg + = None + | RequestToCloseModal + | Saved Contribution + + +update : AppContext -> ProjectRef -> AccountSummary -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef account msg model = + case ( msg, model.form, model.action ) of + ( CloseModal, _, _ ) -> + ( model, Cmd.none, RequestToCloseModal ) + + ( FetchRecentBranchesFinished recentBranches, _, Edit contrib ) -> + case recentBranches of + Ok rb -> + let + form : Form + form = + { title = contrib.title + , description = Maybe.withDefault "" contrib.description + , sourceBranchRef = Just contrib.sourceBranchRef + , targetBranchRef = BranchRef.main_ + , selectBranchSheet = Closed + , save = NotAsked + , recentBranches = rb + , validity = NotChecked + } + in + ( { model | form = Success form }, Cmd.none, None ) + + Err e -> + ( { model | form = Failure e }, Cmd.none, None ) + + ( FetchRecentBranchesFinished recentBranches, _, Create ) -> + case recentBranches of + Ok rb -> + let + sourceBranchRef = + rb.ownContributorBranches + |> List.head + |> Maybe.map .ref + + form = + { title = "" + , description = "" + , sourceBranchRef = sourceBranchRef + , targetBranchRef = BranchRef.main_ + , selectBranchSheet = Closed + , save = NotAsked + , recentBranches = rb + , validity = NotChecked + } + in + ( { model | form = Success form }, Cmd.none, None ) + + Err e -> + ( { model | form = Failure e }, Cmd.none, None ) + + ( ToggleSelectSourceBranchSheet, Success form, _ ) -> + let + form_ = + let + filter = + if Account.hasProjectAccess projectRef account then + ShareApi.AllBranches (Just account.handle) + + else + ShareApi.ContributorBranches (Just account.handle) + + selectBranchSheet = + case form.selectBranchSheet of + OpenForSource _ -> + Closed + + OpenForTarget _ -> + Closed + + Closed -> + OpenForSource { sheet = SearchBranchSheet.init filter } + in + { form | selectBranchSheet = selectBranchSheet } + in + ( { model | form = Success form_ }, Cmd.none, None ) + + ( ToggleSelectTargetBranchSheet, Success form, _ ) -> + let + form_ = + let + selectBranchSheet = + case form.selectBranchSheet of + OpenForSource _ -> + Closed + + OpenForTarget _ -> + Closed + + Closed -> + OpenForTarget { sheet = SearchBranchSheet.init ShareApi.ProjectBranches } + in + { form | selectBranchSheet = selectBranchSheet } + in + ( { model | form = Success form_ }, Cmd.none, None ) + + ( SearchBranchSheetMsg sbsMsg, Success form, _ ) -> + case form.selectBranchSheet of + OpenForSource s -> + let + ( sheet_, cmd, sbsOut ) = + SearchBranchSheet.update appContext projectRef sbsMsg s.sheet + + newForm = + case ( sbsOut, RemoteData.map .selectBranchSheet model.form ) of + ( SearchBranchSheet.SelectBranchRequest branch, Success (OpenForSource _) ) -> + { form | sourceBranchRef = Just branch.ref, selectBranchSheet = Closed } + + ( SearchBranchSheet.SelectBranchRequest branch, Success (OpenForTarget _) ) -> + { form | targetBranchRef = branch.ref, selectBranchSheet = Closed } + + _ -> + { form | selectBranchSheet = OpenForSource { s | sheet = sheet_ } } + in + ( { model | form = Success newForm } + , Cmd.map SearchBranchSheetMsg cmd + , None + ) + + OpenForTarget t -> + let + ( sheet_, cmd, sbsOut ) = + SearchBranchSheet.update appContext projectRef sbsMsg t.sheet + + newForm = + case sbsOut of + SearchBranchSheet.NoOutMsg -> + { form | selectBranchSheet = OpenForTarget { t | sheet = sheet_ } } + + SearchBranchSheet.SelectBranchRequest branch -> + { form | sourceBranchRef = Just branch.ref, selectBranchSheet = Closed } + in + ( { model | form = Success newForm } + , Cmd.map SearchBranchSheetMsg cmd + , None + ) + + Closed -> + ( model, Cmd.none, None ) + + ( UpdateSubmissionTitle t, Success form, _ ) -> + let + newForm = + { form | title = t } + in + ( { model | form = Success newForm }, Cmd.none, None ) + + ( UpdateSubmissionDescription d, Success form, _ ) -> + let + newForm = + { form | description = d } + in + ( { model | form = Success newForm }, Cmd.none, None ) + + ( SaveContribution, Success form, Create ) -> + let + validity = + case ( form.sourceBranchRef, form.title ) of + ( Nothing, "" ) -> + Invalid { needsTitle = True, sourceBranch = Missing } + + ( Nothing, _ ) -> + Invalid { needsTitle = False, sourceBranch = Missing } + + ( Just sbr, "" ) -> + if BranchRef.equals sbr form.targetBranchRef then + Invalid { needsTitle = True, sourceBranch = SameAsTarget } + + else + Invalid { needsTitle = True, sourceBranch = ValidSourceBranch } + + ( Just sbr, _ ) -> + if BranchRef.equals sbr form.targetBranchRef then + Invalid { needsTitle = False, sourceBranch = SameAsTarget } + + else + Valid { sourceBranchRef = sbr } + in + case validity of + Valid { sourceBranchRef } -> + if not (String.isEmpty form.title) then + let + newForm = + { form | save = Loading } + + newContribution = + { title = form.title + , description = StringE.nonEmpty form.description + , status = ContributionStatus.InReview + , sourceBranchRef = sourceBranchRef + , targetBranchRef = form.targetBranchRef + } + in + ( { model | form = Success newForm } + , createProjectContribution appContext projectRef newContribution + , None + ) + + else + ( model, Cmd.none, None ) + + _ -> + ( { model | form = Success { form | validity = validity } }, Cmd.none, None ) + + ( SaveContribution, Success form, Edit c ) -> + case form.sourceBranchRef of + Just sourceBranchRef -> + let + newForm = + { form | save = Loading } + + updatedContribution = + { title = form.title + , description = StringE.nonEmpty form.description + , status = c.status + , sourceBranchRef = sourceBranchRef + , targetBranchRef = form.targetBranchRef + } + in + ( { model | form = Success newForm } + , updateProjectContribution appContext + projectRef + c.ref + updatedContribution + , None + ) + + _ -> + ( model, Cmd.none, None ) + + ( SaveContributionFinished contribution, Success form, _ ) -> + let + cmd = + case contribution of + Success c -> + Util.delayMsg 1500 (SuccessfullySaved c) + + _ -> + Cmd.none + in + ( { model | form = Success { form | save = contribution } }, cmd, None ) + + ( SuccessfullySaved contrib, _, _ ) -> + ( model, Cmd.none, Saved contrib ) + + _ -> + ( model, Cmd.none, None ) + + + +-- EFFECTS + + +fetchRecentBranches : AppContext -> AccountSummary -> ProjectRef -> Task Http.Error RecentBranches +fetchRecentBranches appContext currentUser projectRef = + let + contributorBranchesParams = + { kind = ShareApi.ContributorBranches (Just currentUser.handle) + , searchQuery = Nothing + , limit = 3 + , cursor = Nothing + } + + projectBranchesParams = + { kind = ShareApi.ProjectBranches + , searchQuery = Nothing + , limit = 3 + , cursor = Nothing + } + in + Task.map2 + RecentBranches + (fetchBranches appContext projectRef contributorBranchesParams) + (fetchBranches appContext projectRef projectBranchesParams) + + +fetchBranches : + AppContext + -> ProjectRef + -> ShareApi.ProjectBranchesParams + -> Task Http.Error (List BranchSummary) +fetchBranches appContext projectRef params = + HttpApi.toTask appContext.api.url + (Decode.field "items" (Decode.list BranchSummary.decode)) + (ShareApi.projectBranches projectRef params) + + +createProjectContribution : AppContext -> ProjectRef -> ShareApi.NewProjectContribution -> Cmd Msg +createProjectContribution appContext projectRef newContribution = + ShareApi.createProjectContribution projectRef newContribution + |> HttpApi.toRequest + Contribution.decode + (RemoteData.fromResult >> SaveContributionFinished) + |> HttpApi.perform appContext.api + + +updateProjectContribution : + AppContext + -> ProjectRef + -> ContributionRef + -> + { title : String + , description : Maybe String + , status : ContributionStatus + , sourceBranchRef : BranchRef + , targetBranchRef : BranchRef + } + -> Cmd Msg +updateProjectContribution appContext projectRef contribRef updates = + ShareApi.updateProjectContribution projectRef contribRef (ShareApi.ProjectContributionUpdate updates) + |> HttpApi.toRequest + Contribution.decode + (RemoteData.fromResult >> SaveContributionFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewSheet : SelectBranchOpenSheet -> List BranchSummary -> Html Msg +viewSheet sheet suggestions = + let + viewSuggestions brs = + case brs of + [] -> + [] + + _ -> + [ SearchBranchSheet.viewBranchList "Most recent branches" brs ] + + suggestions_ = + { data = Success suggestions + , view = viewSuggestions + } + in + sheet.sheet + |> SearchBranchSheet.view "Select Branch" suggestions_ Nothing + |> Html.map SearchBranchSheetMsg + + +viewSelectSourceBranchAnchoredOverlay : SelectBranchSheet -> Maybe BranchRef -> List BranchSummary -> AnchoredOverlay Msg +viewSelectSourceBranchAnchoredOverlay selectBranchSheet branchRef ownContributorBranches = + let + button caret = + Button.iconThenLabel ToggleSelectSourceBranchSheet + Icon.branch + (MaybeE.unwrap "Select Branch" BranchRef.toString branchRef) + |> Button.withIconAfterLabel caret + |> Button.small + |> Button.view + + ao_ = + AnchoredOverlay.anchoredOverlay ToggleSelectSourceBranchSheet + in + case selectBranchSheet of + Closed -> + ao_ (button Icon.caretDown) + + OpenForSource sheet -> + ao_ (button Icon.caretUp) + |> AnchoredOverlay.withSheet (AnchoredOverlay.sheet (viewSheet sheet ownContributorBranches)) + |> AnchoredOverlay.withSheetPosition AnchoredOverlay.BottomLeft + + OpenForTarget _ -> + ao_ (button Icon.caretDown) + + +viewSelectTargetBranchAnchoredOverlay : SelectBranchSheet -> BranchRef -> List BranchSummary -> AnchoredOverlay Msg +viewSelectTargetBranchAnchoredOverlay selectBranchSheet branchRef projectBranches = + let + button caret = + Button.iconThenLabel ToggleSelectTargetBranchSheet + Icon.branch + (BranchRef.toString branchRef) + |> Button.withIconAfterLabel caret + |> Button.small + |> Button.view + + ao_ = + AnchoredOverlay.anchoredOverlay ToggleSelectSourceBranchSheet + in + case selectBranchSheet of + Closed -> + ao_ (button Icon.caretDown) + + OpenForSource _ -> + ao_ (button Icon.caretDown) + + OpenForTarget sheet -> + ao_ (button Icon.caretUp) + |> AnchoredOverlay.withSheet (AnchoredOverlay.sheet (viewSheet sheet projectBranches)) + |> AnchoredOverlay.withSheetPosition AnchoredOverlay.BottomLeft + + +view : ProjectRef -> String -> Model -> Html Msg +view projectRef title model = + case model.form of + Success form -> + let + titleValidity tf = + case form.validity of + Invalid { needsTitle } -> + if needsTitle then + TextField.markAsInvalid tf + + else + tf + + _ -> + tf + + sourceBranchInvalid = + case form.validity of + Invalid { sourceBranch } -> + case sourceBranch of + Missing -> + span [ class "source-branch_invalid" ] + [ Icon.view Icon.arrowLeftUp + , text "Please provide a source branch." + ] + + SameAsTarget -> + span [ class "source-branch_invalid" ] + [ Icon.view Icon.arrowLeftUp + , text "Please provide different source and target branches." + ] + + _ -> + UI.nothing + + _ -> + UI.nothing + + viewForm = + div [ class "form" ] + [ section [ class "branches" ] + [ text "Request to merge" + , span [ class "source-branch" ] + [ AnchoredOverlay.view + (viewSelectSourceBranchAnchoredOverlay + form.selectBranchSheet + form.sourceBranchRef + form.recentBranches.ownContributorBranches + ) + , sourceBranchInvalid + ] + , text "into" + , AnchoredOverlay.view + (viewSelectTargetBranchAnchoredOverlay + form.selectBranchSheet + form.targetBranchRef + form.recentBranches.projectBranches + ) + ] + , section [ class "fields" ] + [ TextField.field UpdateSubmissionTitle "Title" form.title + |> TextField.withAutofocus + |> TextField.withHelpText "Required. Ex. 'Add List.map'" + |> titleValidity + |> TextField.view + , TextField.field UpdateSubmissionDescription "Description" form.description + |> TextField.withHelpText "Provide a detailed description of your contribution; what it solves and how." + |> TextField.withRows 8 + |> TextField.view + ] + , aside [ class "small-print" ] + [ span [ class "info-icon" ] [ Icon.view Icon.info ] + , p [] + [ text "By submitting this contribution, you agree to license it using " + , strong [] [ text (ProjectRef.toString projectRef) ] + , text "'s existing license." + , text " Please read the " + , Link.view "Terms of Service" Link.termsOfService + , text " for more detail." + ] + ] + ] + + ( content, leftSideFooter, dimOverlay ) = + case form.save of + NotAsked -> + ( viewForm, [], False ) + + Loading -> + ( viewForm, [ StatusBanner.working "Saving.." ], True ) + + Success contrib -> + let + status = + case model.action of + Create -> + StatusMessage.good + ("Successfully created contribution " ++ ContributionRef.toString contrib.ref) + [] + + Edit _ -> + StatusMessage.good + ("Successfully updated contribution " ++ ContributionRef.toString contrib.ref) + [] + in + ( div [ class "saved" ] [ StatusMessage.view status ], [], False ) + + Failure _ -> + ( viewForm, [ StatusBanner.bad "Couldn't save, please try again." ], False ) + in + content + |> Modal.content + |> Modal.modal "project-contribution-form-modal" CloseModal + |> Modal.withActionsIf + [ Button.button CloseModal "Cancel" + |> Button.medium + |> Button.subdued + , Button.iconThenLabel SaveContribution Icon.rocket title + |> Button.medium + |> Button.positive + ] + (not (RemoteData.isSuccess form.save)) + |> Modal.withLeftSideFooter leftSideFooter + |> Modal.withDimOverlay dimOverlay + |> Modal.withHeaderIf "Save contribution" (not (RemoteData.isSuccess form.save)) + |> Modal.view + + _ -> + Modal.content UI.nothing + |> Modal.modal "submit-contribution-modal" CloseModal + |> Modal.withHeader "Save contribution" + |> Modal.view diff --git a/src/UnisonShare/ProjectTicketFormModal.elm b/src/UnisonShare/ProjectTicketFormModal.elm new file mode 100644 index 00000000..325859f7 --- /dev/null +++ b/src/UnisonShare/ProjectTicketFormModal.elm @@ -0,0 +1,325 @@ +module UnisonShare.ProjectTicketFormModal exposing + ( FormAction(..) + , Model + , Msg + , OutMsg(..) + , init + , update + , view + ) + +import Html exposing (Html, div, section) +import Html.Attributes exposing (class) +import Lib.HttpApi as HttpApi +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import UI.Button as Button +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.Modal as Modal +import UI.StatusBanner as StatusBanner +import UI.StatusMessage as StatusMessage +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Ticket as Ticket exposing (Ticket) +import UnisonShare.Ticket.TicketRef as TicketRef exposing (TicketRef) +import UnisonShare.Ticket.TicketStatus as TicketStatus exposing (TicketStatus) + + + +-- MODEL + + +type Validity + = NotChecked + | Valid + | Invalid + { needsTitle : Bool + , needsDescription : Bool + } + + +type alias Form = + { title : String + , description : String + , save : WebData Ticket + , validity : Validity + } + + +type FormAction + = Edit Ticket + | Create + + +type alias Model = + { form : Form + , action : FormAction + } + + +init : FormAction -> Model +init action = + let + form = + case action of + Edit ticket -> + { title = ticket.title + , description = ticket.description + , save = NotAsked + , validity = NotChecked + } + + Create -> + { title = "" + , description = "" + , save = NotAsked + , validity = NotChecked + } + in + { form = form, action = action } + + + +-- UPDATE + + +type Msg + = CloseModal + | UpdateSubmissionTitle String + | UpdateSubmissionDescription String + | SaveTicket + | SaveTicketFinished (WebData Ticket) + | SuccessfullySaved Ticket + + +type OutMsg + = None + | RequestToCloseModal + | Saved Ticket + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef msg model = + let + form = + model.form + in + case ( msg, model.action ) of + ( CloseModal, _ ) -> + ( model, Cmd.none, RequestToCloseModal ) + + ( UpdateSubmissionTitle t, _ ) -> + let + newForm = + { form | title = t } + in + ( { model | form = newForm }, Cmd.none, None ) + + ( UpdateSubmissionDescription d, _ ) -> + let + newForm = + { form | description = d } + in + ( { model | form = newForm }, Cmd.none, None ) + + ( SaveTicket, Create ) -> + let + validity = + case ( form.title, form.description ) of + ( "", "" ) -> + Invalid { needsTitle = True, needsDescription = True } + + ( "", _ ) -> + Invalid { needsTitle = True, needsDescription = False } + + ( _, "" ) -> + Invalid { needsTitle = False, needsDescription = True } + + _ -> + Valid + in + case validity of + Valid -> + if not (String.isEmpty form.title) then + let + newForm = + { form | save = Loading } + + newTicket = + { title = form.title + , description = form.description + , status = TicketStatus.Open + } + in + ( { model | form = newForm } + , createProjectTicket appContext projectRef newTicket + , None + ) + + else + ( model, Cmd.none, None ) + + _ -> + ( { model | form = { form | validity = validity } }, Cmd.none, None ) + + ( SaveTicket, Edit c ) -> + let + newForm = + { form | save = Loading } + + updatedTicket = + { title = form.title + , description = form.description + , status = c.status + } + in + ( { model | form = newForm } + , updateProjectTicket appContext + projectRef + c.ref + updatedTicket + , None + ) + + ( SaveTicketFinished ticket, _ ) -> + let + cmd = + case ticket of + Success t -> + Util.delayMsg 1500 (SuccessfullySaved t) + + _ -> + Cmd.none + in + ( { model | form = { form | save = ticket } }, cmd, None ) + + ( SuccessfullySaved ticket, _ ) -> + ( model, Cmd.none, Saved ticket ) + + + +-- EFFECTS + + +createProjectTicket : AppContext -> ProjectRef -> ShareApi.NewProjectTicket -> Cmd Msg +createProjectTicket appContext projectRef newTicket = + ShareApi.createProjectTicket projectRef newTicket + |> HttpApi.toRequest + Ticket.decode + (RemoteData.fromResult >> SaveTicketFinished) + |> HttpApi.perform appContext.api + + +updateProjectTicket : + AppContext + -> ProjectRef + -> TicketRef + -> + { title : String + , description : String + , status : TicketStatus + } + -> Cmd Msg +updateProjectTicket appContext projectRef ticketRef updates = + ShareApi.updateProjectTicket projectRef ticketRef (ShareApi.ProjectTicketUpdate updates) + |> HttpApi.toRequest + Ticket.decode + (RemoteData.fromResult >> SaveTicketFinished) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +view : String -> Model -> Html Msg +view title model = + let + form = + model.form + + titleValidity tf = + case form.validity of + Invalid { needsTitle } -> + if needsTitle then + TextField.markAsInvalid tf + + else + tf + + _ -> + tf + + descriptionValidity tf = + case form.validity of + Invalid { needsDescription } -> + if needsDescription then + TextField.markAsInvalid tf + + else + tf + + _ -> + tf + + viewForm = + div [ class "form" ] + [ section [ class "fields" ] + [ TextField.field UpdateSubmissionTitle "Title" form.title + |> TextField.withAutofocus + |> TextField.withHelpText "Required. Ex. 'Bug in List.map'" + |> titleValidity + |> TextField.view + , TextField.field UpdateSubmissionDescription "Description" form.description + |> TextField.withHelpText "Provide a detailed description of your feedback." + |> TextField.withRows 8 + |> descriptionValidity + |> TextField.view + ] + ] + + ( content, leftSideFooter, dimOverlay ) = + case form.save of + NotAsked -> + ( viewForm, [], False ) + + Loading -> + ( viewForm, [ StatusBanner.working "Saving.." ], True ) + + Success ticket -> + let + status = + case model.action of + Create -> + StatusMessage.good + ("Successfully created ticket " ++ TicketRef.toString ticket.ref) + [] + + Edit _ -> + StatusMessage.good + ("Successfully updated ticket " ++ TicketRef.toString ticket.ref) + [] + in + ( div [ class "saved" ] [ StatusMessage.view status ], [], False ) + + Failure _ -> + ( viewForm, [ StatusBanner.bad "Couldn't save, please try again." ], False ) + in + content + |> Modal.content + |> Modal.modal "project-ticket-form-modal" CloseModal + |> Modal.withActionsIf + [ Button.button CloseModal "Cancel" + |> Button.medium + |> Button.subdued + , Button.iconThenLabel SaveTicket Icon.rocket title + |> Button.medium + |> Button.positive + ] + (not (RemoteData.isSuccess form.save)) + |> Modal.withLeftSideFooter leftSideFooter + |> Modal.withDimOverlay dimOverlay + |> Modal.withHeaderIf "Save ticket" (not (RemoteData.isSuccess form.save)) + |> Modal.view diff --git a/src/UnisonShare/PublishProjectReleaseModal.elm b/src/UnisonShare/PublishProjectReleaseModal.elm new file mode 100644 index 00000000..35f6e4b9 --- /dev/null +++ b/src/UnisonShare/PublishProjectReleaseModal.elm @@ -0,0 +1,611 @@ +module UnisonShare.PublishProjectReleaseModal exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.Definition.Doc as Doc exposing (Doc) +import Code.Perspective as Perspective +import Code.Version as Version exposing (Version) +import Html exposing (Html, div, footer, h1, h3, header, p, section, small, strong, text) +import Html.Attributes exposing (class, classList) +import Http +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import List.Nonempty as NEL +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.AnchoredOverlay as AnchoredOverlay exposing (AnchoredOverlay) +import UI.Button as Button +import UI.Form.RadioField as RadioField +import UI.Icon as Icon +import UI.Modal as Modal +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext as AppContext exposing (AppContext) +import UnisonShare.BranchSummary as BranchSummary exposing (BranchSummary) +import UnisonShare.CodeBrowsingContext as CodeBrowsingContext +import UnisonShare.InteractiveDoc as InteractiveDoc +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Project.Release as Release exposing (Release) +import UnisonShare.SearchBranchSheet as SearchBranchSheet + + +type alias SelectBranchOpenSheet = + { sheet : SearchBranchSheet.Model } + + +type SelectBranchSheet + = Open SelectBranchOpenSheet + | Closed + + +type alias ReleaseNotes = + { doc : Doc, interactiveDoc : InteractiveDoc.Model, maximized : Bool } + + +type alias SourceBranch = + { branchRef : BranchRef + , branch : WebData BranchSummary + , releaseNotes : WebData (Maybe ReleaseNotes) + } + + +type alias Model = + { sourceBranch : SourceBranch + , version : Version + , selectBranchSheet : SelectBranchSheet + , publishedRelease : WebData Release + , latestBranches : WebData (List BranchSummary) + } + + +init : AppContext -> ProjectRef -> Version -> Maybe BranchSummary -> ( Model, Cmd Msg ) +init appContext projectRef current draftBranch = + case draftBranch of + Just b -> + let + version = + case b.ref of + BranchRef.ReleaseDraftBranchRef v -> + Version.clampToNextValid current v + + _ -> + Version.nextMajor current + in + ( { sourceBranch = + { branchRef = b.ref, branch = Success b, releaseNotes = Loading } + , version = version + , selectBranchSheet = Closed + , publishedRelease = RemoteData.NotAsked + , latestBranches = RemoteData.Loading + } + , Cmd.batch + [ fetchBranchReleaseNotes appContext projectRef b.ref + , fetchLatestBranches appContext projectRef + ] + ) + + Nothing -> + ( { sourceBranch = + { branchRef = BranchRef.main_, branch = Loading, releaseNotes = Loading } + , version = Version.nextMajor current + , selectBranchSheet = Closed + , publishedRelease = RemoteData.NotAsked + , latestBranches = RemoteData.Loading + } + , Cmd.batch + [ fetchMainBranch appContext projectRef + , fetchBranchReleaseNotes appContext projectRef BranchRef.main_ + , fetchLatestBranches appContext projectRef + ] + ) + + + +-- UPDATE + + +type Msg + = CloseModal + | FetchMainBranchFinished (WebData BranchSummary) + | FetchReleaseNotesFinished BranchRef (HttpResult (Maybe Doc)) + | FetchLatestBranchesFinished (WebData (List BranchSummary)) + | ToggleSelectBranchSheet + | UpdateVersion Version + | SearchBranchSheetMsg SearchBranchSheet.Msg + | Publish + | PublishFinished (WebData Release) + | ToggleReleaseNotesSize + | InteractiveDocMsg InteractiveDoc.Msg + + +type OutMsg + = NoOutMsg + | RequestCloseModal + | Published Release + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef msg model = + case msg of + FetchMainBranchFinished mainBranch -> + if BranchRef.equals model.sourceBranch.branchRef BranchRef.main_ then + let + sourceBranch = + model.sourceBranch + + sourceBranch_ = + { sourceBranch | branch = mainBranch } + in + ( { model | sourceBranch = sourceBranch_ }, Cmd.none, NoOutMsg ) + + else + ( model, Cmd.none, NoOutMsg ) + + FetchLatestBranchesFinished latestBranches -> + ( { model | latestBranches = latestBranches }, Cmd.none, NoOutMsg ) + + FetchReleaseNotesFinished branchRef doc -> + if BranchRef.equals model.sourceBranch.branchRef branchRef then + let + sourceBranch = + model.sourceBranch + + sourceBranch_ rn = + { sourceBranch | releaseNotes = rn } + in + case doc of + Ok d -> + ( { model | sourceBranch = sourceBranch_ (Success (Maybe.map releaseNotes d)) }, Cmd.none, NoOutMsg ) + + Err (Http.BadStatus 404) -> + ( { model | sourceBranch = sourceBranch_ (Success Nothing) }, Cmd.none, NoOutMsg ) + + Err e -> + ( { model | sourceBranch = sourceBranch_ (Failure e) }, Cmd.none, NoOutMsg ) + + else + ( model, Cmd.none, NoOutMsg ) + + CloseModal -> + ( model, Cmd.none, RequestCloseModal ) + + UpdateVersion version -> + ( { model | version = version }, Cmd.none, NoOutMsg ) + + ToggleSelectBranchSheet -> + let + selectBranchSheet = + case model.selectBranchSheet of + Open _ -> + Closed + + Closed -> + Open { sheet = SearchBranchSheet.init ShareApi.ProjectBranches } + in + ( { model | selectBranchSheet = selectBranchSheet } + , Cmd.none + , NoOutMsg + ) + + SearchBranchSheetMsg sbsMsg -> + case model.selectBranchSheet of + Open s -> + let + ( sheet_, cmd, sbsOut ) = + SearchBranchSheet.update appContext projectRef sbsMsg s.sheet + + ( newModel, releaseNotesCmd ) = + case sbsOut of + SearchBranchSheet.NoOutMsg -> + ( { model | selectBranchSheet = Open { s | sheet = sheet_ } }, Cmd.none ) + + SearchBranchSheet.SelectBranchRequest branch -> + let + sourceBranch = + model.sourceBranch + + sourceBranch_ = + { sourceBranch + | branchRef = branch.ref + , branch = Success branch + , releaseNotes = Loading + } + in + ( { model | sourceBranch = sourceBranch_, selectBranchSheet = Closed }, fetchBranchReleaseNotes appContext projectRef branch.ref ) + in + ( newModel + , Cmd.batch [ Cmd.map SearchBranchSheetMsg cmd, releaseNotesCmd ] + , NoOutMsg + ) + + Closed -> + ( model, Cmd.none, NoOutMsg ) + + Publish -> + case model.sourceBranch.branch of + Success br -> + ( { model | publishedRelease = Loading } + , publishProjectRelease appContext projectRef br model.version + , NoOutMsg + ) + + _ -> + ( model, Cmd.none, NoOutMsg ) + + PublishFinished release -> + let + ( cmd, out ) = + case release of + Success r -> + ( Util.delayMsg 1500 CloseModal, Published r ) + + _ -> + ( Cmd.none, NoOutMsg ) + in + ( { model | publishedRelease = release }, cmd, out ) + + ToggleReleaseNotesSize -> + case model.sourceBranch.releaseNotes of + Success (Just rn) -> + let + sourceBranch = + model.sourceBranch + + sourceBranch_ = + { sourceBranch | releaseNotes = Success (Just { rn | maximized = not rn.maximized }) } + in + ( { model | sourceBranch = sourceBranch_ } + , Cmd.none + , NoOutMsg + ) + + _ -> + ( model, Cmd.none, NoOutMsg ) + + InteractiveDocMsg dMsg -> + case model.sourceBranch.releaseNotes of + Success (Just rn) -> + let + config = + AppContext.toCodeConfig + appContext + (CodeBrowsingContext.project projectRef model.sourceBranch.branchRef) + Perspective.relativeRootPerspective + + -- TODO: Should we allow navigation from release notes? + ( id, idCmd, _ ) = + InteractiveDoc.update config dMsg rn.interactiveDoc + + sourceBranch = + model.sourceBranch + + sourceBranch_ = + { sourceBranch | releaseNotes = Success (Just { rn | interactiveDoc = id }) } + in + ( { model | sourceBranch = sourceBranch_ } + , Cmd.map InteractiveDocMsg idCmd + , NoOutMsg + ) + + _ -> + ( model, Cmd.none, NoOutMsg ) + + + +-- EFFECTS + + +fetchMainBranch : AppContext -> ProjectRef -> Cmd Msg +fetchMainBranch appContext projectRef = + ShareApi.projectBranch projectRef BranchRef.main_ + |> HttpApi.toRequest BranchSummary.decode (RemoteData.fromResult >> FetchMainBranchFinished) + |> HttpApi.perform appContext.api + + +fetchLatestBranches : AppContext -> ProjectRef -> Cmd Msg +fetchLatestBranches appContext projectRef = + ShareApi.projectBranches projectRef + { kind = ShareApi.ProjectBranches + , searchQuery = Nothing + , limit = 3 + , cursor = Nothing + } + |> HttpApi.toRequest (Decode.field "items" (Decode.list BranchSummary.decode)) + (RemoteData.fromResult >> FetchLatestBranchesFinished) + |> HttpApi.perform appContext.api + + +fetchBranchReleaseNotes : AppContext -> ProjectRef -> BranchRef -> Cmd Msg +fetchBranchReleaseNotes appContext projectRef branchRef = + ShareApi.projectBranchReleaseNotes projectRef branchRef + |> HttpApi.toRequest + (Decode.field "doc" (Decode.nullable Doc.decode)) + (FetchReleaseNotesFinished branchRef) + |> HttpApi.perform appContext.api + + +publishProjectRelease : AppContext -> ProjectRef -> BranchSummary -> Version -> Cmd Msg +publishProjectRelease appContext projectRef branch version = + let + data = + { branchRef = branch.ref + , causalHash = branch.causalHash + , version = version + } + in + ShareApi.createProjectRelease projectRef data + |> HttpApi.toRequest Release.decode (RemoteData.fromResult >> PublishFinished) + |> HttpApi.perform appContext.api + + + +-- HELPERS + + +releaseNotes : Doc -> ReleaseNotes +releaseNotes d = + { doc = d, interactiveDoc = InteractiveDoc.init, maximized = False } + + + +-- VIEW + + +viewSheet : WebData (List BranchSummary) -> SelectBranchOpenSheet -> Html Msg +viewSheet suggestions sheet = + let + viewSuggestions data = + [ SearchBranchSheet.viewBranchList "Latest branches" data ] + + suggestions_ = + { data = suggestions + , view = viewSuggestions + } + in + sheet.sheet + |> SearchBranchSheet.view "Select source branch" suggestions_ Nothing + |> Html.map SearchBranchSheetMsg + + +viewSelectBranchAnchoredOverlay : WebData (List BranchSummary) -> SelectBranchSheet -> BranchSummary -> AnchoredOverlay Msg +viewSelectBranchAnchoredOverlay suggestions selectBranchSheet branch = + let + button caret = + Button.iconThenLabel ToggleSelectBranchSheet Icon.branch (BranchRef.toString branch.ref) + |> Button.withIconAfterLabel caret + |> Button.medium + |> Button.view + + ao_ = + AnchoredOverlay.anchoredOverlay ToggleSelectBranchSheet + in + case selectBranchSheet of + Closed -> + ao_ (button Icon.caretDown) + + Open sheet -> + ao_ (button Icon.caretUp) + |> AnchoredOverlay.withSheet (AnchoredOverlay.sheet (viewSheet suggestions sheet)) + + +viewLoadingModalContent : Html msg +viewLoadingModalContent = + div [ class "publish-project-release_loading" ] + [ div [ class "publish-project-release_loading_group" ] + [ Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Small |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Large |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Huge |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Small |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Large |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.view + ] + , div [ class "publish-project-release_loading_group" ] + [ Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Small |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Large |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Huge |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Small |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Large |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.view + ] + ] + + +viewReleaseNotes : BranchRef -> WebData (Maybe ReleaseNotes) -> Html Msg +viewReleaseNotes branchRef releaseNotes_ = + let + path = + "ReleaseNotes Doc on " ++ BranchRef.toString branchRef + + minMaxToggle rn = + if rn.maximized then + Button.icon ToggleReleaseNotesSize Icon.minimize + + else + Button.icon ToggleReleaseNotesSize Icon.maximize + + viewTitle t right = + header [ class "release-notes-preview_header" ] + [ div [ class "release-notes-preview_title" ] [ Icon.view Icon.doc, text t ] + , right + ] + + releaseNotesLoading = + [ viewTitle ("Looking up " ++ path) UI.nothing + , div [ class "release-notes-preview_loading" ] + [ div + [ class "release-notes-preview_loading_items" ] + [ Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Small |> Placeholder.subdued |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Large |> Placeholder.subdued |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.subdued |> Placeholder.view + ] + , div + [ class "release-notes-preview_loading_items" ] + [ Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.subdued |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Small |> Placeholder.subdued |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Large |> Placeholder.subdued |> Placeholder.view + , Placeholder.text |> Placeholder.withLength Placeholder.Medium |> Placeholder.subdued |> Placeholder.view + ] + ] + ] + + content = + case releaseNotes_ of + NotAsked -> + releaseNotesLoading + + Loading -> + releaseNotesLoading + + Success (Just rn) -> + [ viewTitle path (minMaxToggle rn |> Button.small |> Button.view) + , Html.map InteractiveDocMsg (InteractiveDoc.view rn.interactiveDoc rn.doc) + ] + + Success Nothing -> + [ viewTitle ("Couldn't find a " ++ path) UI.nothing + ] + + Failure _ -> + [] + in + div [ class "release-notes-preview" ] content + + +view : Version -> Model -> Html Msg +view currentVersion model = + let + nextMajor = + Version.nextMajor currentVersion + + nextMinor = + Version.nextMinor currentVersion + + nextPatch = + Version.nextPatch currentVersion + + versionOptions = + RadioField.field + "release-version" + UpdateVersion + (NEL.Nonempty + (RadioField.option + ("Next Major: " ++ Version.toString nextMajor) + "For backwards incompatible changes" + nextMajor + ) + [ RadioField.option + ("Next Minor: " ++ Version.toString nextMinor) + "For backwards compatible changes" + nextMinor + , RadioField.option + ("Next Patch: " ++ Version.toString nextPatch) + "For backwards compatible bug fixes" + nextPatch + ] + ) + model.version + + prepare statusBanner disabled = + case model.sourceBranch.branch of + NotAsked -> + viewLoadingModalContent + + Loading -> + viewLoadingModalContent + + Success sourceBranch -> + let + releaseNotesMaximized = + model.sourceBranch.releaseNotes + |> RemoteData.map (Maybe.map .maximized) + |> RemoteData.map (Maybe.withDefault False) + |> RemoteData.withDefault False + in + div + [ class "publish-project-release_content" + , classList + [ ( "publish-project-release_content_disabled", disabled ) + , ( "publish-project-release_release-notes-preview-maximized", releaseNotesMaximized ) + ] + ] + [ div [ class "publish-project-release_form-and-release-notes" ] + [ section [ class "publish-project-release_form" ] + [ section [ class "publish-project-release_select-branch" ] + [ h3 [] [ text "Source Branch" ] + , AnchoredOverlay.view + (viewSelectBranchAnchoredOverlay + model.latestBranches + model.selectBranchSheet + sourceBranch + ) + , small [ class "help" ] [ text "The release will be based on the latest change in the selected branch." ] + ] + , section [ class "publish-project-release_select-version" ] + [ h3 [] [ text "Version" ] + , RadioField.view versionOptions + ] + ] + , section [ class "publish-project-release_release-notes" ] + [ h3 [] [ text "Release Notes" ] + , p [] + [ text "Release notes are automatically included if a " + , strong [] [ text "Doc" ] + , text " term named " + , strong [] [ text "ReleaseNotes" ] + , text " exist on the selected source branch." + ] + , viewReleaseNotes + model.sourceBranch.branchRef + model.sourceBranch.releaseNotes + ] + ] + , footer [ class "publish-project-release_footer" ] + [ statusBanner + , Button.button + CloseModal + "Cancel" + |> Button.medium + |> Button.subdued + |> Button.view + , Button.iconThenLabel Publish Icon.rocket "Publish" + |> Button.medium + |> Button.positive + |> Button.view + ] + ] + + Failure _ -> + div [ class "publish-project-release_error" ] + [ StatusBanner.bad + "Something broke on our end and we couldn't start the publish release process. Please try again." + ] + + content = + case model.publishedRelease of + NotAsked -> + prepare UI.nothing False + + Loading -> + prepare (StatusBanner.working "Publishing..") True + + Success _ -> + div [ class "publish-project-release_publish-success" ] + [ div [ class "publish-project-release_publish-success_emoji" ] [ text "🥳" ] + , h1 [] [ text "Congrats!!" ] + , p [] [ text "You just released ", strong [] [ text (Version.toString model.version) ] ] + ] + + Failure _ -> + prepare (StatusBanner.bad "Couldn't publish release, please try again.") False + in + content + |> Modal.content + |> Modal.modal "publish-project-release-modal" CloseModal + |> Modal.withHeader "Cut a new release" + |> Modal.view diff --git a/src/UnisonShare/Route.elm b/src/UnisonShare/Route.elm new file mode 100644 index 00000000..d84e78eb --- /dev/null +++ b/src/UnisonShare/Route.elm @@ -0,0 +1,890 @@ +module UnisonShare.Route exposing + ( CodeRoute(..) + , ProjectContributionRoute(..) + , ProjectRoute(..) + , Route(..) + , UserRoute(..) + , acceptTerms + , account + , catalog + , cloud + , codeRoot + , definition + , fromUrl + , navigate + , privacyPolicy + , projectBranch + , projectBranchDefinition + , projectBranchRoot + , projectBranches + , projectContribution + , projectContributionChanges + , projectContributions + , projectOverview + , projectRelease + , projectReleases + , projectSettings + , projectTicket + , projectTickets + , replacePerspective + , termsOfService + , toRoute + , toUrlPattern + , toUrlString + , ucmConnected + , userCode + , userCodeRoot + , userContributions + , userDefinition + , userProfile + ) + +import Browser.Navigation as Nav +import Code.BranchRef as BranchRef exposing (BranchRef) +import Code.Definition.Reference exposing (Reference(..)) +import Code.FullyQualifiedName as FQN +import Code.Hash as Hash +import Code.HashQualified exposing (HashQualified(..)) +import Code.Perspective as Perspective exposing (Perspective, PerspectiveParams) +import Code.UrlParsers + exposing + ( b + , branchRef + , end + , perspectiveParams + , projectSlug + , reference + , s + , slash + , userHandle + , version + ) +import Code.Version as Version exposing (Version) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import List.Nonempty as NEL +import Parser exposing ((|.), (|=), Parser, oneOf, succeed, symbol) +import UI.ViewMode as ViewMode exposing (ViewMode) +import UnisonShare.AppError as AppError exposing (AppError) +import UnisonShare.Contribution.ContributionRef as ContributionRef exposing (ContributionRef) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Ticket.TicketRef as TicketRef exposing (TicketRef) +import Url exposing (Url) +import Url.Builder exposing (relative, string) + + +type CodeRoute + = CodeRoot PerspectiveParams + | Definition PerspectiveParams Reference + + +type UserRoute + = UserProfile + | UserContributions + | UserCode ViewMode CodeRoute + + +type ProjectContributionRoute + = ProjectContributionOverview + | ProjectContributionChanges + + +type ProjectRoute + = ProjectOverview + | ProjectBranches + | ProjectBranch BranchRef ViewMode CodeRoute + | ProjectRelease Version + | ProjectReleases + | ProjectTicket TicketRef + | ProjectTickets + | ProjectContribution ContributionRef ProjectContributionRoute + | ProjectContributions + | ProjectSettings + + +type Route + = Catalog + | Account + | User UserHandle UserRoute + | Project ProjectRef ProjectRoute + | TermsOfService + | AcceptTerms (Maybe Url) + | PrivacyPolicy + | UcmConnected + | Cloud + | NotFound String + | Error AppError + + + +-- CREATE --------------------------------------------------------------------- + + +catalog : Route +catalog = + Catalog + + +account : Route +account = + Account + + +cloud : Route +cloud = + Cloud + + +userProfile : UserHandle -> Route +userProfile handle_ = + User handle_ UserProfile + + +userCode : UserHandle -> CodeRoute -> Route +userCode handle_ codeRoute = + User handle_ (UserCode ViewMode.Regular codeRoute) + + +userDefinition : UserHandle -> Perspective -> Reference -> Route +userDefinition handle_ pers ref = + let + pp = + Perspective.toParams pers + in + User handle_ (UserCode ViewMode.Regular (Definition pp ref)) + + +userCodeRoot : UserHandle -> Perspective -> Route +userCodeRoot handle_ pers = + let + pp = + Perspective.toParams pers + in + User handle_ (UserCode ViewMode.Regular (CodeRoot pp)) + + +userContributions : UserHandle -> Route +userContributions handle_ = + User handle_ UserContributions + + +projectOverview : ProjectRef -> Route +projectOverview projectRef_ = + Project projectRef_ ProjectOverview + + +projectBranch : ProjectRef -> BranchRef -> CodeRoute -> Route +projectBranch projectRef_ branchRef_ codeRoute = + Project projectRef_ (ProjectBranch branchRef_ ViewMode.Regular codeRoute) + + +projectBranches : ProjectRef -> Route +projectBranches projectRef_ = + Project projectRef_ ProjectBranches + + +projectRelease : ProjectRef -> Version -> Route +projectRelease projectRef_ version = + Project projectRef_ (ProjectRelease version) + + +projectReleases : ProjectRef -> Route +projectReleases projectRef_ = + Project projectRef_ ProjectReleases + + +projectContribution : ProjectRef -> ContributionRef -> Route +projectContribution projectRef_ contribRef = + Project projectRef_ (ProjectContribution contribRef ProjectContributionOverview) + + +projectContributionChanges : ProjectRef -> ContributionRef -> Route +projectContributionChanges projectRef_ contribRef = + Project projectRef_ (ProjectContribution contribRef ProjectContributionChanges) + + +projectContributions : ProjectRef -> Route +projectContributions projectRef_ = + Project projectRef_ ProjectContributions + + +projectTicket : ProjectRef -> TicketRef -> Route +projectTicket projectRef_ ticketRef = + Project projectRef_ (ProjectTicket ticketRef) + + +projectTickets : ProjectRef -> Route +projectTickets projectRef_ = + Project projectRef_ ProjectTickets + + +projectSettings : ProjectRef -> Route +projectSettings projectRef_ = + Project projectRef_ ProjectSettings + + +projectBranchDefinition : ProjectRef -> BranchRef -> Perspective -> Reference -> Route +projectBranchDefinition projectRef_ branchRef_ pers ref = + let + pp = + Perspective.toParams pers + in + Project projectRef_ (ProjectBranch branchRef_ ViewMode.Regular (Definition pp ref)) + + +projectBranchRoot : ProjectRef -> BranchRef -> Perspective -> Route +projectBranchRoot projectRef_ branchRef pers = + let + pp = + Perspective.toParams pers + in + Project projectRef_ (ProjectBranch branchRef ViewMode.Regular (CodeRoot pp)) + + +definition : Perspective -> Reference -> CodeRoute +definition pers ref = + Definition (Perspective.toParams pers) ref + + +codeRoot : Perspective -> CodeRoute +codeRoot pers = + CodeRoot (Perspective.toParams pers) + + +replacePerspective : Maybe Reference -> Perspective -> CodeRoute +replacePerspective ref pers = + let + pp = + Perspective.toParams pers + in + case ref of + Just r -> + Definition pp r + + Nothing -> + CodeRoot pp + + +termsOfService : Route +termsOfService = + TermsOfService + + +acceptTerms : Maybe Url -> Route +acceptTerms continueUrl = + AcceptTerms continueUrl + + +privacyPolicy : Route +privacyPolicy = + PrivacyPolicy + + +ucmConnected : Route +ucmConnected = + UcmConnected + + + +-- PARSE ---------------------------------------------------------------------- + + +toRoute : Maybe String -> Parser Route +toRoute queryString = + oneOf + [ b homeParser + , b catalogParser + , b accountParser + , b (userParser queryString) + , b (projectParser queryString) -- Specifically comes _after_ userParser because project slugs share the url space with user pages + , b termsOfServiceParser + , b (acceptTermsParser queryString) + , b privacyPolicyParser + , b ucmConnectedParser + , b cloudParser + , b (errorParser queryString) + ] + + +homeParser : Parser Route +homeParser = + succeed Catalog |. end + + +catalogParser : Parser Route +catalogParser = + succeed Catalog |. slash |. s "catalog" + + +accountParser : Parser Route +accountParser = + succeed Account |. slash |. s "account" + + +termsOfServiceParser : Parser Route +termsOfServiceParser = + succeed TermsOfService |. slash |. s "terms-of-service" + + +acceptTermsParser : Maybe String -> Parser Route +acceptTermsParser queryString = + let + urlParser : Parser Url + urlParser = + let + parseMaybe url = + case url of + Just s_ -> + Parser.succeed s_ + + Nothing -> + Parser.problem "Invalid Url" + in + Parser.chompUntilEndOr "&" + |> Parser.getChompedString + |> Parser.map Url.fromString + |> Parser.andThen parseMaybe + + continueUrlParser : Parser Url + continueUrlParser = + succeed identity |. s "continue" |. symbol "=" |= urlParser |. end + + continueUrl : Maybe Url + continueUrl = + queryString + |> Maybe.withDefault "" + |> Parser.run continueUrlParser + |> Result.toMaybe + in + succeed (AcceptTerms continueUrl) |. slash |. s "accept-terms" + + +privacyPolicyParser : Parser Route +privacyPolicyParser = + succeed PrivacyPolicy |. slash |. s "privacy-policy" + + +ucmConnectedParser : Parser Route +ucmConnectedParser = + succeed UcmConnected |. slash |. s "ucm-connected" + + +cloudParser : Parser Route +cloudParser = + succeed Cloud |. slash |. s "cloud" + + +errorParser : Maybe String -> Parser Route +errorParser queryString = + let + appErrorQueryParamParser : Parser AppError + appErrorQueryParamParser = + oneOf + [ b (succeed AppError.AccountCreationGitHubPermissionsRejected |. s "appError=AccountCreationGitHubPermissionsRejected") + , b (succeed AppError.AccountCreationHandleAlreadyTaken |. s "appError=AccountCreationHandleAlreadyTaken") + , b (succeed AppError.UnspecifiedError |. s "appError=UnspecifiedError") + , b (succeed AppError.UnspecifiedError) + ] + + appError : AppError + appError = + queryString + |> Maybe.withDefault "" + |> Parser.run appErrorQueryParamParser + |> Result.withDefault AppError.UnspecifiedError + in + succeed (Error appError) |. slash |. s "error" + + +viewModeQueryParamParser : Parser ViewMode +viewModeQueryParamParser = + oneOf + [ b (succeed ViewMode.Regular |. s "viewMode=regular") + , b (succeed ViewMode.Presentation |. s "viewMode=presentation") + , b (succeed ViewMode.Regular) + ] + + +userParser : Maybe String -> Parser Route +userParser queryString = + let + viewMode : ViewMode + viewMode = + queryString + |> Maybe.withDefault "" + |> Parser.run viewModeQueryParamParser + |> Result.withDefault ViewMode.Regular + + userCode_ h c = + User h (UserCode viewMode c) + + userContrib h = + User h UserContributions + + userProfile_ h = + User h UserProfile + in + oneOf + [ b (succeed userProfile_ |. slash |= userHandle |. end) + , b (succeed userContrib |. slash |= userHandle |. slash |. s "p" |. slash |. s "contributions" |. end) + , b (succeed userCode_ |. slash |= userHandle |. slash |. s "code" |. slash |= codeParser) + , b (succeed userCode_ |. slash |= userHandle |. slash |. s "p" |. slash |. s "code" |. slash |= codeParser) + ] + + +projectParser : Maybe String -> Parser Route +projectParser queryString = + let + viewMode = + queryString + |> Maybe.withDefault "" + |> Parser.run viewModeQueryParamParser + |> Result.withDefault ViewMode.Regular + + projectOverview_ handle slug = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps ProjectOverview + + projectBranches_ handle slug = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps ProjectBranches + + projectBranch_ handle slug branchRef c = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectBranch branchRef viewMode c) + + projectBranchRoot_ handle slug branchRef = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectBranch branchRef viewMode (CodeRoot (Perspective.ByRoot Perspective.Relative))) + + projectRelease_ handle slug version_ = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectRelease version_) + + projectReleases_ handle slug = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps ProjectReleases + + projectSettings_ handle slug = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps ProjectSettings + + projectContribution_ handle slug ref = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectContribution ref ProjectContributionOverview) + + projectContributionChanges_ handle slug ref = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectContribution ref ProjectContributionChanges) + + projectContributions_ handle slug = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps ProjectContributions + + projectTicket_ handle slug ref = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectTicket ref) + + projectTickets_ handle slug = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps ProjectTickets + in + oneOf + [ b (succeed projectOverview_ |. slash |= userHandle |. slash |= projectSlug |. end) + , b (succeed projectBranches_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "branches" |. end) + , b (succeed projectBranch_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "code" |. slash |= branchRef |. slash |= codeParser) + , b (succeed projectBranchRoot_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "code" |. slash |= branchRef |. end) + , b (succeed projectRelease_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "releases" |. slash |= version |. end) + , b (succeed projectReleases_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "releases" |. end) + , b (succeed projectContributionChanges_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "contributions" |. slash |= ContributionRef.fromUrl |. slash |. s "changes" |. end) + , b (succeed projectContribution_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "contributions" |. slash |= ContributionRef.fromUrl |. end) + , b (succeed projectContributions_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "contributions" |. end) + , b (succeed projectTicket_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "tickets" |. slash |= TicketRef.fromUrl |. end) + , b (succeed projectTickets_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "tickets" |. end) + , b (succeed projectSettings_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "settings" |. end) + ] + + +codeParser : Parser CodeRoute +codeParser = + oneOf [ b codeDefinitionParser, b codeRootParser ] + + +codeRootParser : Parser CodeRoute +codeRootParser = + succeed CodeRoot |= perspectiveParams + + +codeDefinitionParser : Parser CodeRoute +codeDefinitionParser = + succeed Definition |= perspectiveParams |. slash |= reference + + +{-| In environments like Unison Local, the UI is served with a base path + +This means that a route to a definition might look like: + + - "/:some-token/ui/latest/terms/base/List/map" + (where "/:some-token/ui/" is the base path.) + +The base path is determined outside of the Elm app using the tag in the + section of the document. The Browser uses this tag to prefix all links. + +The base path must end in a slash for links to work correctly, but our parser +expects a path to starts with a slash. When parsing the URL we thus pre-process +the path to strip the base path and ensure a slash prefix before we parse. + +--- + +Note that this doesn't use Url.Parser to parse the URL as you'd normally see in +Elm apps. This is because of how we represent Fully Qualified Names in the url +by turning `.` into `/`: + +`base.data.List.map` is represented in the url as `base/data/List/map` + +-} +fromUrl : String -> Url -> Route +fromUrl basePath url = + let + stripBasePath path = + if basePath == "/" then + path + + else + String.replace basePath "" path + + ensureSlashPrefix path = + if String.startsWith "/" path then + path + + else + "/" ++ path + + parse queryString path = + path + |> Parser.run (toRoute queryString) + |> Result.withDefault (NotFound path) + in + url + |> .path + |> stripBasePath + |> ensureSlashPrefix + |> parse url.query + + + +-- HELPERS -------------------------------------------------------------------- + + +{-| Creates the string of a route in a de-parameritized way for deduping pages in metrics events +-} +toUrlPattern : Route -> String +toUrlPattern r = + let + codePattern codeRoute = + case codeRoute of + CodeRoot (Perspective.ByRoot _) -> + "" + + CodeRoot (Perspective.ByNamespace _ _) -> + "/latest/namespaces/:fqn" + + Definition (Perspective.ByRoot _) ref -> + "/latest/" ++ refPattern ref + + Definition (Perspective.ByNamespace _ _) ref -> + "/latest/namespaces/:fqn/;/" ++ refPattern ref + + refPattern ref = + case ref of + TypeReference _ -> + "types/:fqn" + + TermReference _ -> + "terms/:fqn" + + AbilityConstructorReference _ -> + "ability-constructors/:fqn" + + DataConstructorReference _ -> + "data-constructors/:fqn" + in + case r of + Catalog -> + "catalog" + + Account -> + "account" + + User _ UserProfile -> + ":handle" + + User _ (UserCode _ codeRoute) -> + ":handle/p/code" ++ codePattern codeRoute + + User _ UserContributions -> + ":handle/p/contributions" + + Project _ ProjectOverview -> + ":handle/:project-slug" + + Project _ ProjectBranches -> + ":handle/:project-slug/branches" + + Project _ (ProjectBranch _ _ codeRoute) -> + ":handle/:project-slug/code/:branch-ref/" ++ codePattern codeRoute + + Project _ (ProjectRelease _) -> + ":handle/:project-slug/releases/:version" + + Project _ ProjectReleases -> + ":handle/:project-slug/releases" + + Project _ (ProjectContribution _ ProjectContributionOverview) -> + ":handle/:project-slug/contributions/:contribution-ref" + + Project _ (ProjectContribution _ ProjectContributionChanges) -> + ":handle/:project-slug/contributions/:contribution-ref/changes" + + Project _ ProjectContributions -> + ":handle/:project-slug/contributions" + + Project _ (ProjectTicket _) -> + ":handle/:project-slug/tickets/:ticket-ref" + + Project _ ProjectTickets -> + ":handle/:project-slug/tickets" + + Project _ ProjectSettings -> + ":handle/:project-slug/settings" + + TermsOfService -> + "terms-of-service" + + AcceptTerms _ -> + "accept-terms" + + PrivacyPolicy -> + "privacy-policy" + + UcmConnected -> + "ucm-connected" + + Cloud -> + "cloud" + + Error _ -> + "error" + + NotFound _ -> + "404" + + +toUrlString : Route -> String +toUrlString route = + let + namespaceSuffix = + ";" + + hqToPath hq = + case hq of + NameOnly fqn -> + fqn |> FQN.toUrlSegments |> NEL.toList + + HashOnly h -> + [ Hash.toUrlString h ] + + HashQualified _ h -> + -- TODO: Currently not supported, since we favor the hash + -- because HashQualified url parsing is broken + [ Hash.toUrlString h ] + + refPath ref = + case ref of + TypeReference hq -> + "types" :: hqToPath hq + + TermReference hq -> + "terms" :: hqToPath hq + + AbilityConstructorReference hq -> + "ability-constructors" :: hqToPath hq + + DataConstructorReference hq -> + "data-constructors" :: hqToPath hq + + perspectiveParamsToPath pp includeNamespacesSuffix = + case pp of + Perspective.ByRoot Perspective.Relative -> + [ "latest" ] + + Perspective.ByRoot (Perspective.Absolute hash) -> + [ Hash.toUrlString hash ] + + Perspective.ByNamespace Perspective.Relative fqn -> + if includeNamespacesSuffix then + "latest" :: "namespaces" :: NEL.toList (FQN.segments fqn) ++ [ namespaceSuffix ] + + else + "latest" :: "namespaces" :: NEL.toList (FQN.segments fqn) + + -- Currently the model supports Absolute URLs (aka Permalinks), + -- but we don't use it since Unison Share does not support any + -- history, meaning that everytime we deploy Unison Share, the + -- previous versions of the codebase are lost. + -- It's fully intended for this feature to be brought back + Perspective.ByNamespace (Perspective.Absolute hash) fqn -> + if includeNamespacesSuffix then + Hash.toUrlString hash :: "namespaces" :: NEL.toList (FQN.segments fqn) ++ [ namespaceSuffix ] + + else + Hash.toUrlString hash :: "namespaces" :: NEL.toList (FQN.segments fqn) + + codePath codeRoute = + case codeRoute of + CodeRoot params -> + perspectiveParamsToPath params False + + Definition params ref -> + perspectiveParamsToPath params True ++ refPath ref + + ( path, queryParams ) = + case route of + Catalog -> + ( [ "catalog" ], [] ) + + Account -> + ( [ "account" ], [] ) + + User handle_ UserProfile -> + ( [ UserHandle.toString handle_ ], [] ) + + User handle_ (UserCode vm codeRoute) -> + let + path_ = + [ UserHandle.toString handle_, "p", "code" ] ++ codePath codeRoute + in + if ViewMode.isPresentation vm then + ( path_, [ string "viewMode" (ViewMode.toString vm) ] ) + + else + ( path_, [] ) + + User handle_ UserContributions -> + ( [ UserHandle.toString handle_, "p", "contributions" ], [] ) + + Project projectRef_ ProjectOverview -> + ( ProjectRef.toUrlPath projectRef_, [] ) + + Project projectRef_ ProjectBranches -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "branches" ], [] ) + + Project projectRef_ (ProjectBranch branchRef vm codeRoute) -> + let + path_ = + ProjectRef.toUrlPath projectRef_ + ++ "code" + :: BranchRef.toUrlPath branchRef + ++ codePath codeRoute + in + if ViewMode.isPresentation vm then + ( path_, [ string "viewMode" (ViewMode.toString vm) ] ) + + else + ( path_, [] ) + + Project projectRef_ (ProjectRelease v) -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "releases", Version.toString v ], [] ) + + Project projectRef_ ProjectReleases -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "releases" ], [] ) + + Project projectRef_ (ProjectContribution r ProjectContributionOverview) -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "contributions", ContributionRef.toUrlString r ], [] ) + + Project projectRef_ (ProjectContribution r ProjectContributionChanges) -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "contributions", ContributionRef.toUrlString r, "changes" ], [] ) + + Project projectRef_ ProjectContributions -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "contributions" ], [] ) + + Project projectRef_ (ProjectTicket r) -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "tickets", TicketRef.toUrlString r ], [] ) + + Project projectRef_ ProjectTickets -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "tickets" ], [] ) + + Project projectRef_ ProjectSettings -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "settings" ], [] ) + + TermsOfService -> + ( [ "terms-of-service" ], [] ) + + AcceptTerms (Just continueUrl) -> + ( [ "accept-terms" ], [ string "continue" (Url.toString continueUrl) ] ) + + AcceptTerms Nothing -> + ( [ "accept-terms" ], [] ) + + PrivacyPolicy -> + ( [ "privacy-policy" ], [] ) + + UcmConnected -> + ( [ "ucm-connected" ], [] ) + + Cloud -> + ( [ "cloud" ], [] ) + + Error e -> + ( [ "error" ], [ string "appError" (AppError.toString e) ] ) + + NotFound _ -> + ( [ "catalog" ], [] ) + in + relative path queryParams + + + +-- EFFECTS + + +navigate : Nav.Key -> Route -> Cmd msg +navigate navKey route = + route + |> toUrlString + |> Nav.pushUrl navKey diff --git a/src/UnisonShare/SearchBranchSheet.elm b/src/UnisonShare/SearchBranchSheet.elm new file mode 100644 index 00000000..36b2d9bb --- /dev/null +++ b/src/UnisonShare/SearchBranchSheet.elm @@ -0,0 +1,307 @@ +module UnisonShare.SearchBranchSheet exposing (..) + +import Code.BranchRef as BranchRef +import Html exposing (Html, article, div, footer, h2, h3, h4, p, section, text) +import Html.Attributes exposing (class) +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Search as Search exposing (Search) +import Lib.SearchResults as SearchResults +import RemoteData exposing (WebData) +import UI +import UI.Click as Click +import UI.Divider as Divider +import UI.EmptyState as EmptyState +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.KeyboardShortcut.KeyboardEvent as KeyboardEvent +import UI.Placeholder as Placeholder +import UI.StatusBanner as StatusBanner +import UI.Tag as Tag +import UnisonShare.Api as ShareApi exposing (ProjectBranchesKindFilter) +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.BranchSummary as BranchSummary exposing (BranchSummary) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) + + + +-- MODEL + + +type alias Model = + { search : Search BranchSummary + , branchKindFilter : ProjectBranchesKindFilter + } + + +init : ProjectBranchesKindFilter -> Model +init kindFilter = + { search = Search.empty + , branchKindFilter = kindFilter + } + + + +-- UPDATE + + +type Msg + = UpdateSearchQuery String + | PerformSearch String + | ClearSearch + | FetchSearchResultsFinished String (HttpResult (List BranchSummary)) + | SelectBranch BranchSummary + | NoOp + + +type OutMsg + = NoOutMsg + | SelectBranchRequest BranchSummary + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef msg model = + case msg of + UpdateSearchQuery q -> + let + newModel = + { model | search = Search.withQuery q model.search } + + ( sheet, cmd ) = + if Search.hasSubstantialQuery model.search then + ( newModel, Search.debounce (PerformSearch q) ) + + else + ( newModel, Cmd.none ) + in + ( sheet, cmd, NoOutMsg ) + + PerformSearch query -> + if Search.queryEquals query model.search then + ( { model | search = Search.Searching query Nothing } + , fetchBranches (FetchSearchResultsFinished query) + appContext + projectRef + { kind = model.branchKindFilter + , searchQuery = Just query + , limit = 10 + , cursor = Nothing + } + , NoOutMsg + ) + + else + ( model, Cmd.none, NoOutMsg ) + + ClearSearch -> + ( { model | search = Search.reset model.search }, Cmd.none, NoOutMsg ) + + FetchSearchResultsFinished query branches -> + if Search.queryEquals query model.search then + ( { model | search = Search.fromResult model.search branches }, Cmd.none, NoOutMsg ) + + else + ( model, Cmd.none, NoOutMsg ) + + NoOp -> + ( model, Cmd.none, NoOutMsg ) + + SelectBranch branch -> + ( model, Cmd.none, SelectBranchRequest branch ) + + + +-- EFFECTS + + +fetchBranches : + (HttpResult (List BranchSummary) -> Msg) + -> AppContext + -> ProjectRef + -> ShareApi.ProjectBranchesParams + -> Cmd Msg +fetchBranches doneMsg appContext projectRef params = + ShareApi.projectBranches projectRef params + |> HttpApi.toRequest + (Decode.field "items" (Decode.list BranchSummary.decode)) + doneMsg + |> HttpApi.perform appContext.api + + + +-- VIEW +-- really this should be a, but alas no higher kinded types + + +type alias Suggestions a = + { data : WebData a + , view : a -> List (Html Msg) + } + + +viewBranch : BranchSummary -> Html Msg +viewBranch branch = + BranchRef.toTag branch.ref + |> Tag.large + |> Tag.withClick (Click.onClick (SelectBranch branch)) + |> Tag.view + + +viewBranchList : String -> List BranchSummary -> Html Msg +viewBranchList title branches = + section [ class "search-branch switch-branch_branch-list" ] + [ h3 [] [ text title ] + , div [ class "search-branch-sheet_branch-list_items" ] (List.map viewBranch branches) + ] + + +view : String -> Suggestions a -> Maybe (Html Msg) -> Model -> Html Msg +view title suggestions footer_ model = + let + shape_ length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.tiny + + shape length = + shape_ length + |> Placeholder.subdued + |> Placeholder.view + + shapeBright length = + shape_ length + |> Placeholder.view + + loading = + [ div [ class "search-branch-sheet_recent-branches_loading" ] + [ div [ class "search-branch-sheet_branches_loading-section" ] + [ shape Placeholder.Small + , div [ class "search-branch-sheet_branches_loading-list" ] + [ shapeBright Placeholder.Tiny + , shapeBright Placeholder.Medium + , shapeBright Placeholder.Small + ] + ] + ] + ] + + viewSuggestions data = + case suggestions.view data of + [] -> + [] + + xs -> + [ div [ class "search-branch-sheet_suggestions" ] xs ] + + ( content, isSearching, query ) = + case ( model.search, suggestions.data ) of + ( Search.NotAsked q, RemoteData.Success data ) -> + ( viewSuggestions data + , False + , q + ) + + ( Search.NotAsked q, RemoteData.Failure _ ) -> + ( [ StatusBanner.bad "Something broke on our end and we couldn't load the recent branches.\nPlease try again." ], False, q ) + + ( Search.NotAsked q, RemoteData.NotAsked ) -> + ( loading, False, q ) + + ( Search.NotAsked q, RemoteData.Loading ) -> + ( loading, False, q ) + + ( Search.Searching q _, RemoteData.Success data ) -> + ( viewSuggestions data + ++ [ div [ class "search-branch-sheet_searching" ] [] ] + , True + , q + ) + + ( Search.Searching q _, _ ) -> + ( [ div [ class "search-branch-sheet_searching" ] + [ div [ class "search-branch-sheet_branches_loading-list" ] + [ shapeBright Placeholder.Medium + , shapeBright Placeholder.Tiny + , shapeBright Placeholder.Small + ] + ] + ] + , True + , q + ) + + ( Search.Success q sr, suggestions_ ) -> + let + results = + if SearchResults.isEmpty sr then + if q == "" then + case suggestions_ of + RemoteData.Success data -> + viewSuggestions data + + _ -> + [] + + else + [ EmptyState.search + |> EmptyState.onDark + |> EmptyState.withContent + [ div [ class "search-branch-sheet_no-results_message" ] + [ h4 [] [ text "No matches" ] + , p [] [ text ("We looked everywhere, but couldn't find any branches matching \"" ++ q ++ "\".") ] + ] + ] + |> EmptyState.view + ] + + else + [ viewBranchList "Search results" (SearchResults.toList sr) ] + in + ( results, False, q ) + + ( Search.Failure q _, _ ) -> + ( [ StatusBanner.bad "Something broke on our end and we couldn't perform the search. Please try again." + ] + , False + , q + ) + + content_ = + if List.isEmpty content then + [] + + else + [ Divider.divider + |> Divider.small + |> Divider.onDark + |> Divider.withoutMargin + |> Divider.view + , div [ class "search-branch-sheet_branches" ] content + ] + + -- Currently this exists to prevent other keyboard centric interactions + -- to take over from writing in the input field. + keyboardEvent = + KeyboardEvent.on KeyboardEvent.Keydown (always NoOp) + |> KeyboardEvent.stopPropagation + |> KeyboardEvent.attach + + footer__ = + footer_ + |> Maybe.map (\f -> footer [ class "search-branch-sheet_more-link" ] [ f ]) + |> Maybe.withDefault UI.nothing + in + article [ class "search-branch-sheet", keyboardEvent ] + [ h2 [] [ text title ] + , Html.node "search" + [] + ((TextField.fieldWithoutLabel UpdateSearchQuery "Search Branches" query + |> TextField.withHelpText "Find a contributor branch by prefixing their handle, ex: \"@unison\"." + |> TextField.withIconOrWorking Icon.search isSearching + |> TextField.withClear ClearSearch + |> TextField.view + ) + :: content_ + ) + , footer__ + ] diff --git a/src/UnisonShare/Session.elm b/src/UnisonShare/Session.elm new file mode 100644 index 00000000..0b3409cd --- /dev/null +++ b/src/UnisonShare/Session.elm @@ -0,0 +1,109 @@ +module UnisonShare.Session exposing + ( Session(..) + , account + , decode + , handle + , hasProjectAccess + , isHandle + , isOrganizationMember + , isProjectOwner + , isSignedIn + , isUnisonMember + ) + +import Json.Decode as Decode +import Lib.UserHandle as UserHandle exposing (UserHandle) +import UnisonShare.Account as Account exposing (AccountSummary) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) + + +type Session + = Anonymous + | SignedIn AccountSummary + + +isHandle : UserHandle -> Session -> Bool +isHandle handle_ session = + case session of + Anonymous -> + False + + SignedIn a -> + UserHandle.equals a.handle handle_ + + +isSignedIn : Session -> Bool +isSignedIn session = + case session of + Anonymous -> + False + + SignedIn _ -> + True + + +isProjectOwner : ProjectRef -> Session -> Bool +isProjectOwner projectRef session = + isHandle (ProjectRef.handle projectRef) session + + +hasProjectAccess : ProjectRef -> Session -> Bool +hasProjectAccess projectRef session = + case session of + Anonymous -> + False + + SignedIn a -> + let + projectHandle = + ProjectRef.handle projectRef + in + UserHandle.equals projectHandle a.handle || Account.isOrganizationMember projectHandle a + + +isOrganizationMember : UserHandle -> Session -> Bool +isOrganizationMember orgHandle session = + case session of + Anonymous -> + False + + SignedIn a -> + Account.isOrganizationMember orgHandle a + + +isUnisonMember : Session -> Bool +isUnisonMember session = + case session of + Anonymous -> + False + + SignedIn a -> + Account.isUnisonMember a + + +handle : Session -> Maybe UserHandle +handle session = + case session of + Anonymous -> + Nothing + + SignedIn a -> + Just a.handle + + +account : Session -> Maybe AccountSummary +account session = + case session of + Anonymous -> + Nothing + + SignedIn a -> + Just a + + +decode : Decode.Decoder Session +decode = + Decode.oneOf + [ Decode.map SignedIn Account.decodeSummary + , Decode.succeed Anonymous + ] diff --git a/src/UnisonShare/SetupInstructions.elm b/src/UnisonShare/SetupInstructions.elm new file mode 100644 index 00000000..20f53ade --- /dev/null +++ b/src/UnisonShare/SetupInstructions.elm @@ -0,0 +1,227 @@ +module UnisonShare.SetupInstructions exposing (..) + +import Code.Perspective as Perspective +import Html exposing (Html, div, li, ol, p, span, strong, text) +import Html.Attributes exposing (class) +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.OperatingSystem as OS exposing (OperatingSystem) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import UI +import UI.CopyField as CopyField +import UI.Icon as Icon +import UI.StatusBanner as StatusBanner +import UI.Steps as Steps +import UnisonShare.Account exposing (Account) +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.CodeBrowsingContext as CodeBrowsingContext +import UnisonShare.Link as Link +import UnisonShare.UnisonRelease as UnisonRelease + + + +-- MODEL + + +type alias Model = + { isEmptyCodebase : WebData Bool } + + +init : AppContext -> Account a -> ( Model, Cmd Msg ) +init appContext account = + ( { isEmptyCodebase = Success True } + , checkCodebaseEmptiness appContext account.handle + ) + + + +-- UPDATE + + +type Msg + = NoOp + | CheckCodebaseEmptiness + | EmptinessCheckFinished (HttpResult Bool) + | Done + + +type OutMsg + = Remain + | NoLongerEmpty + | Exit + + +update : AppContext -> Account a -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext account msg model = + case msg of + NoOp -> + ( model, Cmd.none, Remain ) + + CheckCodebaseEmptiness -> + ( model, checkCodebaseEmptiness appContext account.handle, Remain ) + + EmptinessCheckFinished (Ok isEmpty) -> + -- If the codebase is still empty, schedule another check in 5s + if isEmpty then + ( model, Util.delayMsg 5000 CheckCodebaseEmptiness, Remain ) + + else + -- If we're done, update the model and exit after 10s so that we can show the success message briefly. + ( { model | isEmptyCodebase = Success False }, Util.delayMsg 5000 Done, NoLongerEmpty ) + + EmptinessCheckFinished (Err _) -> + -- Somehow the check failed, lets try again in 10s + ( model, Util.delayMsg 10000 CheckCodebaseEmptiness, Remain ) + + Done -> + ( model, Cmd.none, Exit ) + + + +-- EFFECTS + + +checkCodebaseEmptiness : AppContext -> UserHandle -> Cmd Msg +checkCodebaseEmptiness appContext handle = + let + decoder = + -- Parse to "not-empty" since we don't actually care about the values, just that they exist + Decode.map List.isEmpty (Decode.field "namespaceListingChildren" (Decode.list (Decode.succeed "not-empty"))) + in + ShareApi.browseCodebase + (CodeBrowsingContext.UserCode handle) + Perspective.relativeRootPerspective + Nothing + |> HttpApi.toRequest decoder EmptinessCheckFinished + |> HttpApi.perform appContext.api + + + +-- VIEW + + +installUcmStep : OperatingSystem -> Steps.Step Msg +installUcmStep os = + let + release = + UnisonRelease.latest + + macCommand = + UnisonRelease.installForMac release + + linuxCommands = + UnisonRelease.installForLinux release |> String.join "\n" + + install = + case os of + OS.MacOS -> + CopyField.copyField (\_ -> NoOp) macCommand |> CopyField.withPrefix "$" |> CopyField.view + + OS.Linux -> + div [ class "install-instructions" ] + [ strong [] [ text "To install on Linux, run these commands:" ] + , UI.codeBlock [ class "code" ] (text linuxCommands) + ] + + OS.Windows -> + ol [] + [ li [] + [ text "Set your default terminal application to \"Windows Terminal\" for best results. Search for \"Terminal\" in Settings, or follow this " + , Link.view "how-to." (Link.link "https://www.tenforums.com/tutorials/180053-how-change-default-terminal-application-windows-10-a.html") + ] + , li [] + [ text "Download " + , Link.view "UCM" (Link.link (UnisonRelease.windowsUrl release)) + , text " and extract it to a location of your choosing." + ] + , li [] [ text "Run ", UI.inlineCode [] (text "ucm.exe") ] + ] + + _ -> + strong [] + [ text "Download the latest build from GitHub releases: " + , Link.githubRelease release.tag |> Link.view release.name + ] + in + Steps.step "Install UCM" + [ p [] + [ text "The Unison Codebase Manager, or UCM for short, is the main tool for writing Unison programs. Its an interactive CLI that provides access to your local codebase, helps you navigate it, and typechecks your code." ] + , install + , p [ class "browser-hint" ] + [ Icon.view Icon.bulb + , span [] + [ text "Learn more about UCM in the " + , Link.view "Unison Tour" Link.tour + , text "." + ] + ] + ] + + +pushScratchStep : UserHandle -> Steps.Step Msg +pushScratchStep handle_ = + let + handle = + UserHandle.toUnprefixedString handle_ + + exampleCode = + ".yourCode" + + remoteLocation = + handle ++ ".public" ++ exampleCode + + pushCommand = + "push.create " ++ remoteLocation ++ " " ++ exampleCode + in + Steps.step "Push code to Unison Share" + [ p [] + [ text ("Unison Share hosts your code in a public namespace under your handle (@" ++ handle ++ "), an example namespace structure of a local project might look like this:") + ] + , div [ class "example-namespace-structure" ] + [ UI.inlineCode [] (text remoteLocation) + ] + , p [] [ text "While running UCM, push your local code your Unison Share codebase (this page) with this UCM command:" ] + , CopyField.copyField (\_ -> NoOp) pushCommand |> CopyField.withPrefix ".>" |> CopyField.view + , div [ class "browser-hint" ] + [ Icon.view Icon.bulb + , div [] + [ p [] + [ text "This will push all the contents of " + , UI.inlineCode [] (text exampleCode) + , text " to " + , UI.inlineCode [] (text remoteLocation) + , text " in Unison Share." + ] + , p [] [ text "When running this command, a new browser window might open to authenticate UCM with Unison Share." ] + ] + ] + ] + + +view : AppContext -> Account a -> Model -> Html Msg +view appContext account model = + let + steps = + Steps.steps (installUcmStep appContext.operatingSystem) + [ pushScratchStep account.handle + ] + in + case model.isEmptyCodebase of + Success False -> + div [ class "setup-instructions" ] + [ StatusBanner.good "Sweet! You've successfully pushed code to Unison Share." + ] + + _ -> + let + status = + StatusBanner.working "Waiting on your first push—Unison Share isn't much without your code" + in + div [ class "setup-instructions" ] + [ status + , UI.divider + , Steps.view steps + ] diff --git a/src/UnisonShare/SupportChatWidget.elm b/src/UnisonShare/SupportChatWidget.elm new file mode 100644 index 00000000..39bdd08c --- /dev/null +++ b/src/UnisonShare/SupportChatWidget.elm @@ -0,0 +1,20 @@ +module UnisonShare.SupportChatWidget exposing (..) + +import Html exposing (Html, node) +import Html.Attributes exposing (attribute) +import Lib.UserHandle as UserHandle +import Maybe.Extra as MaybeE +import UnisonShare.Account exposing (Account) +import Url + + +view : Account a -> Html msg +view account = + node "support-chat-widget" + (MaybeE.values + [ Maybe.map (attribute "name") account.name + , Just (attribute "handle" (UserHandle.toUnprefixedString account.handle)) + , Maybe.map (Url.toString >> attribute "avatar-url") account.avatarUrl + ] + ) + [] diff --git a/src/UnisonShare/SupportChatWidget.js b/src/UnisonShare/SupportChatWidget.js new file mode 100644 index 00000000..16283871 --- /dev/null +++ b/src/UnisonShare/SupportChatWidget.js @@ -0,0 +1,42 @@ +class SupportChatWidget extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + const name = this.getAttribute("name"); + const handle = this.getAttribute("handle"); + const avatarUrl = this.getAttribute("avatar-url"); + + let avatar; + if (avatarUrl) { + avatar = { + type: "avatar", + image_url: avatarUrl, + }; + } + + this.loadIntercom(() => { + window.Intercom("boot", { + api_base: "https://api-iam.intercom.io", + app_id: "c4a188cp", + name: name, + user_id: handle, + avatar: avatar, + }); + + window.Intercom("showMessages"); + }); + } + + loadIntercom(onLoad) { + let s = document.createElement("script"); + s.type = "text/javascript"; + s.async = true; + s.src = "https://widget.intercom.io/widget/c4a188cp"; + s.addEventListener("load", onLoad); + document.body.appendChild(s); + } +} + +customElements.define("support-chat-widget", SupportChatWidget); diff --git a/src/UnisonShare/SwitchBranch.elm b/src/UnisonShare/SwitchBranch.elm new file mode 100644 index 00000000..a6930d1b --- /dev/null +++ b/src/UnisonShare/SwitchBranch.elm @@ -0,0 +1,254 @@ +module UnisonShare.SwitchBranch exposing (..) + +import Code.BranchRef as BranchRef exposing (BranchRef) +import Html exposing (Html) +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Maybe.Extra as MaybeE +import RemoteData exposing (WebData) +import UI.AnchoredOverlay as AnchoredOverlay exposing (AnchoredOverlay) +import UI.Button as Button +import UI.Icon as Icon +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.BranchSummary as BranchSummary exposing (BranchSummary) +import UnisonShare.Link as Link +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.SearchBranchSheet as SearchBranchSheet +import UnisonShare.Session as Session + + + +-- MODEL + + +type alias RecentBranches = + { -- Its a Maybe, because we might not be signed in + ownContributorBranches : Maybe (WebData (List BranchSummary)) + , projectBranches : WebData (List BranchSummary) + } + + +type alias Sheet = + { sheet : SearchBranchSheet.Model + , recentBranches : RecentBranches + } + + +type Model + = Closed + | Open Sheet + + +init : Model +init = + Closed + + + +-- UPDATE + + +type Msg + = ToggleSheet + | CloseSheet + | FetchOwnContributorBranchesFinished (HttpResult (List BranchSummary)) + | FetchProjectBranchesFinished (HttpResult (List BranchSummary)) + | SearchBranchSheetMsg SearchBranchSheet.Msg + + +type OutMsg + = None + | SwitchToBranchRequest BranchRef + + +update : AppContext -> ProjectRef -> Msg -> Model -> ( Model, Cmd Msg, OutMsg ) +update appContext projectRef msg model = + case ( msg, model ) of + ( ToggleSheet, Closed ) -> + let + ( contributorBranchesCmd, ownContributorBranches ) = + case appContext.session of + Session.Anonymous -> + ( Cmd.none, Nothing ) + + Session.SignedIn { handle } -> + ( fetchBranches FetchOwnContributorBranchesFinished + appContext + projectRef + { kind = ShareApi.ContributorBranches (Just handle) + , searchQuery = Nothing + , limit = 3 + , cursor = Nothing + } + , Just RemoteData.Loading + ) + + sheet = + { sheet = SearchBranchSheet.init (ShareApi.AllBranches Nothing) + , recentBranches = + { ownContributorBranches = ownContributorBranches + , projectBranches = RemoteData.Loading + } + } + + projectBranchesParams = + { kind = ShareApi.ProjectBranches + , searchQuery = Nothing + , limit = 3 + , cursor = Nothing + } + in + ( Open sheet + , Cmd.batch + [ contributorBranchesCmd + , fetchBranches FetchProjectBranchesFinished + appContext + projectRef + projectBranchesParams + ] + , None + ) + + ( ToggleSheet, Open _ ) -> + ( Closed, Cmd.none, None ) + + ( CloseSheet, _ ) -> + ( Closed, Cmd.none, None ) + + ( FetchOwnContributorBranchesFinished branches, Open s ) -> + let + rb = + s.recentBranches + + sheet = + { s | recentBranches = { rb | ownContributorBranches = Just (RemoteData.fromResult branches) } } + in + ( Open sheet, Cmd.none, None ) + + ( FetchProjectBranchesFinished branches, Open s ) -> + let + rb = + s.recentBranches + + sheet = + { s | recentBranches = { rb | projectBranches = RemoteData.fromResult branches } } + in + ( Open sheet, Cmd.none, None ) + + ( SearchBranchSheetMsg sbsMsg, Open sheet ) -> + let + ( newSheet, cmd, sbsOut ) = + SearchBranchSheet.update appContext projectRef sbsMsg sheet.sheet + in + case sbsOut of + SearchBranchSheet.NoOutMsg -> + ( Open { sheet | sheet = newSheet }, Cmd.map SearchBranchSheetMsg cmd, None ) + + SearchBranchSheet.SelectBranchRequest br -> + ( Closed, Cmd.map SearchBranchSheetMsg cmd, SwitchToBranchRequest br.ref ) + + _ -> + ( model, Cmd.none, None ) + + + +-- EFFECTS + + +fetchBranches : + (HttpResult (List BranchSummary) -> Msg) + -> AppContext + -> ProjectRef + -> ShareApi.ProjectBranchesParams + -> Cmd Msg +fetchBranches doneMsg appContext projectRef params = + ShareApi.projectBranches projectRef params + |> HttpApi.toRequest + (Decode.field "items" (Decode.list BranchSummary.decode)) + doneMsg + |> HttpApi.perform appContext.api + + + +-- VIEW + + +type alias SuggestionsData = + { ownContributorBranches : List BranchSummary + , projectBranches : List BranchSummary + } + + +viewSuggestions : SuggestionsData -> List (Html SearchBranchSheet.Msg) +viewSuggestions data = + let + ownContributorBranches = + if List.isEmpty data.ownContributorBranches then + Nothing + + else + Just (SearchBranchSheet.viewBranchList "My contributor branches" data.ownContributorBranches) + + projectBranches = + if List.isEmpty data.projectBranches then + Nothing + + else + Just (SearchBranchSheet.viewBranchList "Most recent project branches" data.projectBranches) + in + MaybeE.values [ ownContributorBranches, projectBranches ] + + +viewSheet : ProjectRef -> Sheet -> Html Msg +viewSheet projectRef sheet = + let + recentBranches = + RemoteData.map2 + (\ownContributorBranches projectBranches -> + { ownContributorBranches = ownContributorBranches + , projectBranches = projectBranches + } + ) + -- If we don't have a `ownContributorBranches`, its because we're not signed in. + -- In that case, we just want to use an empty list. + (Maybe.withDefault (RemoteData.Success []) + sheet.recentBranches.ownContributorBranches + ) + sheet.recentBranches.projectBranches + + suggestions = + { data = recentBranches + , view = viewSuggestions + } + in + Html.map SearchBranchSheetMsg + (SearchBranchSheet.view + "Switch Branch" + suggestions + (Just (Link.view "View all branches" (Link.projectBranches projectRef))) + sheet.sheet + ) + + +toAnchoredOverlay : ProjectRef -> BranchRef -> Model -> AnchoredOverlay Msg +toAnchoredOverlay projectRef branchRef model = + let + button caret = + Button.iconThenLabel ToggleSheet Icon.branch (BranchRef.toString branchRef) + |> Button.withIconAfterLabel caret + |> Button.small + |> Button.stopPropagation + |> Button.view + + ao_ = + AnchoredOverlay.anchoredOverlay CloseSheet + in + case model of + Closed -> + ao_ (button Icon.caretDown) + + Open sheet -> + ao_ (button Icon.caretUp) + |> AnchoredOverlay.withSheetPosition AnchoredOverlay.BottomLeft + |> AnchoredOverlay.withSheet (AnchoredOverlay.sheet (viewSheet projectRef sheet)) diff --git a/src/UnisonShare/Ticket.elm b/src/UnisonShare/Ticket.elm new file mode 100644 index 00000000..524928ec --- /dev/null +++ b/src/UnisonShare/Ticket.elm @@ -0,0 +1,40 @@ +module UnisonShare.Ticket exposing (Ticket, decode) + +import Json.Decode as Decode +import Json.Decode.Pipeline exposing (optional, required) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import UI.DateTime as DateTime exposing (DateTime) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) +import UnisonShare.Ticket.TicketRef as TicketRef exposing (TicketRef) +import UnisonShare.Ticket.TicketStatus as TicketStatus exposing (TicketStatus) + + +type alias Ticket = + { ref : TicketRef + , authorHandle : Maybe UserHandle + , projectRef : ProjectRef + , createdAt : DateTime + , updatedAt : DateTime + , status : TicketStatus + , numComments : Int + , title : String + , description : String + } + + + +-- DECODE + + +decode : Decode.Decoder Ticket +decode = + Decode.succeed Ticket + |> required "number" TicketRef.decode + |> optional "author" (Decode.map Just UserHandle.decodeUnprefixed) Nothing + |> required "projectRef" ProjectRef.decode + |> required "createdAt" DateTime.decode + |> required "updatedAt" DateTime.decode + |> required "status" TicketStatus.decode + |> required "numComments" Decode.int + |> required "title" Decode.string + |> required "description" Decode.string diff --git a/src/UnisonShare/Ticket/TicketEvent.elm b/src/UnisonShare/Ticket/TicketEvent.elm new file mode 100644 index 00000000..ce78027f --- /dev/null +++ b/src/UnisonShare/Ticket/TicketEvent.elm @@ -0,0 +1,52 @@ +module UnisonShare.Ticket.TicketEvent exposing (..) + +import Json.Decode as Decode +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (optional, required) +import UI.DateTime as DateTime +import UnisonShare.Ticket.TicketStatus as TicketStatus exposing (TicketStatus) +import UnisonShare.Timeline.CommentEvent as CommentEvent exposing (CommentDetails, RemovedCommentDetails) +import UnisonShare.Timeline.TimelineEvent exposing (TimelineEventDetails) +import UnisonShare.User as User + + +type TicketEvent + = StatusChange StatusChangeDetails + | Comment CommentDetails + | CommentRemoved RemovedCommentDetails + + +type alias StatusChangeDetails = + TimelineEventDetails + { newStatus : TicketStatus + + -- TODO: Better support the initial change, which currently is implicitly + -- implied by this Maybe + , oldStatus : Maybe TicketStatus + } + + +decodeStatusChangeDetails : Decode.Decoder StatusChangeDetails +decodeStatusChangeDetails = + let + makeStatusChangeDetails newStatus oldStatus timestamp actor = + { newStatus = newStatus + , oldStatus = oldStatus + , timestamp = timestamp + , actor = actor + } + in + Decode.succeed makeStatusChangeDetails + |> required "newStatus" TicketStatus.decode + |> optional "oldStatus" (Decode.map Just TicketStatus.decode) Nothing + |> required "timestamp" DateTime.decode + |> required "actor" User.decodeSummary + + +decode : Decode.Decoder TicketEvent +decode = + Decode.oneOf + [ when (Decode.field "kind" Decode.string) ((==) "statusChange") (Decode.map StatusChange decodeStatusChangeDetails) + , when (Decode.field "kind" Decode.string) ((==) "comment") (Decode.map Comment CommentEvent.decodeCommentDetails) + , when (Decode.field "kind" Decode.string) ((==) "comment") (Decode.map CommentRemoved CommentEvent.decodeRemovedCommentDetails) + ] diff --git a/src/UnisonShare/Ticket/TicketRef.elm b/src/UnisonShare/Ticket/TicketRef.elm new file mode 100644 index 00000000..78fbab14 --- /dev/null +++ b/src/UnisonShare/Ticket/TicketRef.elm @@ -0,0 +1,96 @@ +module UnisonShare.Ticket.TicketRef exposing + ( TicketRef + , decode + , decodeString + , equals + , fromInt + , fromString + , fromUrl + , toApiString + , toString + , toUrlString + , unsafeFromString + ) + +import Json.Decode as Decode +import Lib.Util as Util +import Parser exposing (Parser) + + +type TicketRef + = TicketRef Int + + +fromInt : Int -> Maybe TicketRef +fromInt n = + if n > 0 then + Just (TicketRef n) + + else + Nothing + + +fromString : String -> Maybe TicketRef +fromString s = + s + |> String.toInt + |> Maybe.andThen fromInt + + +unsafeFromString : String -> TicketRef +unsafeFromString s = + fromString s + |> Maybe.withDefault (TicketRef 0) + + +fromUrl : Parser TicketRef +fromUrl = + let + parseMaybe mversion = + case mversion of + Just s_ -> + Parser.succeed s_ + + Nothing -> + Parser.problem "Invalid TicketRef" + in + Parser.chompUntilEndOr "/" + |> Parser.getChompedString + |> Parser.map fromString + |> Parser.andThen parseMaybe + + +toString : TicketRef -> String +toString (TicketRef n) = + "#" ++ String.fromInt n + + +toUrlString : TicketRef -> String +toUrlString (TicketRef n) = + String.fromInt n + + +toApiString : TicketRef -> String +toApiString (TicketRef n) = + String.fromInt n + + +equals : TicketRef -> TicketRef -> Bool +equals (TicketRef a) (TicketRef b) = + a == b + + + +-- DECODE + + +decodeString : Decode.Decoder TicketRef +decodeString = + Decode.map fromString Decode.string + |> Decode.andThen (Util.decodeFailInvalid "Invalid TicketRef") + + +decode : Decode.Decoder TicketRef +decode = + Decode.map fromInt Decode.int + |> Decode.andThen (Util.decodeFailInvalid "Invalid TicketRef") diff --git a/src/UnisonShare/Ticket/TicketStatus.elm b/src/UnisonShare/Ticket/TicketStatus.elm new file mode 100644 index 00000000..bd9caad7 --- /dev/null +++ b/src/UnisonShare/Ticket/TicketStatus.elm @@ -0,0 +1,35 @@ +module UnisonShare.Ticket.TicketStatus exposing + ( TicketStatus(..) + , decode + , toApiString + ) + +import Json.Decode as Decode +import Json.Decode.Extra exposing (when) + + +type TicketStatus + = Open + | Closed + + +toApiString : TicketStatus -> String +toApiString status = + case status of + Open -> + "open" + + Closed -> + "closed" + + + +-- DECODE + + +decode : Decode.Decoder TicketStatus +decode = + Decode.oneOf + [ when Decode.string ((==) "open") (Decode.succeed Open) + , when Decode.string ((==) "closed") (Decode.succeed Closed) + ] diff --git a/src/UnisonShare/Ticket/TicketTimeline.elm b/src/UnisonShare/Ticket/TicketTimeline.elm new file mode 100644 index 00000000..358b139b --- /dev/null +++ b/src/UnisonShare/Ticket/TicketTimeline.elm @@ -0,0 +1,528 @@ +module UnisonShare.Ticket.TicketTimeline exposing (..) + +import Browser.Dom as Dom +import Dict exposing (Dict) +import Html exposing (Html, div) +import Html.Attributes exposing (class) +import Json.Decode as Decode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import Task +import UI.Icon as Icon +import UI.Placeholder as Placeholder +import UnisonShare.Api as ShareApi +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.DateTimeContext exposing (DateTimeContext) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Ticket.TicketEvent as TicketEvent exposing (TicketEvent(..)) +import UnisonShare.Ticket.TicketRef exposing (TicketRef) +import UnisonShare.Ticket.TicketStatus exposing (TicketStatus(..)) +import UnisonShare.Timeline.CommentEvent as CommentEvent exposing (CommentDetails) +import UnisonShare.Timeline.CommentId as CommentId exposing (CommentId) +import UnisonShare.Timeline.StatusChangeEvent as StatusChangeEvent +import UnisonShare.Timeline.TimelineEvent as TimelineEvent + + +type alias TicketTimeline = + List TicketEvent + + +type alias ModifyCommentRequests = + Dict + -- This is a CommentId + String + { original : CommentDetails + , request : CommentEvent.ModifyCommentRequest + } + + +type alias Model = + { timeline : WebData TicketTimeline + , modifyCommentRequests : ModifyCommentRequests + , newComment : CommentEvent.NewComment + } + + +init : AppContext -> ProjectRef -> TicketRef -> ( Model, Cmd Msg ) +init appContext projectRef contribRef = + ( { timeline = Loading + , modifyCommentRequests = Dict.empty + , newComment = CommentEvent.WritingComment "" + } + , fetchTicketTimeline appContext projectRef contribRef + ) + + + +-- UPDATE + + +type Msg + = NoOp + | FetchTicketTimelineFinished (WebData TicketTimeline) + | UpdateNewComment String + | PostNewComment + | PostNewCommentFinished String (HttpResult CommentDetails) + | ResetNewComment + | ShowDeleteCommentConfirmation CommentId + | CancelDeleteComment CommentId + | DeleteComment CommentId + | DeleteCommentFinished CommentId (WebData ()) + | EditComment CommentId + | UpdateEditingComment CommentId String + | SaveEditComment CommentId String + | SaveEditCommentFinished CommentId String (HttpResult CommentDetails) + | CancelEditComment CommentId + + +update : AppContext -> ProjectRef -> TicketRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext projectRef contribRef msg model = + case msg of + NoOp -> + ( model, Cmd.none ) + + FetchTicketTimelineFinished timeline -> + ( { model | timeline = timeline }, Cmd.none ) + + UpdateNewComment text -> + ( { model | newComment = CommentEvent.WritingComment text }, Cmd.none ) + + PostNewComment -> + case model.newComment of + CommentEvent.WritingComment text -> + if not (String.isEmpty text) then + ( { model | newComment = CommentEvent.PostingComment text } + , postTicketComment appContext projectRef contribRef text + ) + + else + ( model, Cmd.none ) + + CommentEvent.CommentPostingFailure text _ -> + if not (String.isEmpty text) then + ( { model | newComment = CommentEvent.PostingComment text } + , postTicketComment appContext projectRef contribRef text + ) + + else + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + PostNewCommentFinished _ (Ok comment) -> + let + commentEvent = + TicketEvent.Comment comment + + timeline = + model.timeline + |> RemoteData.map (\l -> l ++ [ commentEvent ]) + in + ( { model | timeline = timeline, newComment = CommentEvent.CommentPostingSuccess comment } + , Util.delayMsg 2500 ResetNewComment + ) + + PostNewCommentFinished text (Err e) -> + ( { model | newComment = CommentEvent.CommentPostingFailure text e } + , Cmd.none + ) + + ResetNewComment -> + case model.newComment of + CommentEvent.CommentPostingSuccess _ -> + ( { model | newComment = CommentEvent.WritingComment "" }, Cmd.none ) + + _ -> + ( model, Cmd.none ) + + EditComment commentId -> + let + f evt acc = + case evt of + Comment details -> + if CommentId.equals commentId details.id then + Just details + + else + acc + + _ -> + acc + + original = + model.timeline + |> RemoteData.map (List.foldl f Nothing) + |> RemoteData.withDefault Nothing + in + case original of + Just orig -> + let + modifyCommentRequests = + Dict.insert + (CommentId.toString orig.id) + { original = orig + , request = CommentEvent.Editing orig.content + } + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Dom.focus ("edit_" ++ CommentId.toString orig.id) |> Task.attempt (always NoOp) + ) + + _ -> + ( model, Cmd.none ) + + UpdateEditingComment id edit -> + let + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.Editing edit })) + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + SaveEditComment id edit -> + case Dict.get (CommentId.toString id) model.modifyCommentRequests of + Just { original } -> + let + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.SavingEdit edit })) + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , updateTicketComment appContext projectRef contribRef id original.revision edit + ) + + Nothing -> + ( model, Cmd.none ) + + SaveEditCommentFinished id _ (Ok comment) -> + let + replaceComment evt = + case evt of + TicketEvent.Comment c -> + if CommentId.equals c.id id then + TicketEvent.Comment comment + + else + evt + + _ -> + evt + + timeline = + model.timeline + |> RemoteData.map (List.map replaceComment) + + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.EditSaved })) + model.modifyCommentRequests + in + ( { model | timeline = timeline, modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + SaveEditCommentFinished id _ (Err e) -> + let + modifyCommentRequests = + Dict.update + (CommentId.toString id) + (Maybe.map (\r -> { r | request = CommentEvent.EditFailure e })) + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + CancelEditComment id -> + let + modifyCommentRequests = + Dict.remove (CommentId.toString id) model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + ShowDeleteCommentConfirmation commentId -> + let + f evt acc = + case evt of + Comment details -> + if CommentId.equals commentId details.id then + Just details + + else + acc + + _ -> + Nothing + + original = + model.timeline + |> RemoteData.map (List.foldl f Nothing) + |> RemoteData.withDefault Nothing + in + case original of + Just orig -> + let + modifyCommentRequests = + Dict.insert + (CommentId.toString orig.id) + { original = orig + , request = CommentEvent.ConfirmDelete + } + model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + _ -> + ( model, Cmd.none ) + + CancelDeleteComment id -> + let + modifyCommentRequests = + Dict.remove (CommentId.toString id) model.modifyCommentRequests + in + ( { model | modifyCommentRequests = modifyCommentRequests } + , Cmd.none + ) + + DeleteComment commentId -> + let + removedDetails c = + { id = commentId + , timestamp = c.timestamp + , deletedAt = appContext.now + } + + eventToRemoved idToRemove evt ( evts, removed ) = + case evt of + Comment d -> + if CommentId.equals d.id idToRemove then + ( CommentRemoved (removedDetails d) :: evts, Just d ) + + else + ( evt :: evts, removed ) + + _ -> + ( evt :: evts, removed ) + + ( timeline, removedComment ) = + model.timeline + |> RemoteData.map (List.foldr (eventToRemoved commentId) ( [], Nothing )) + |> RemoteData.unwrap ( model.timeline, Nothing ) (\( t, r ) -> ( Success t, r )) + in + case removedComment of + Just c -> + let + modifyCommentRequests = + Dict.insert + (CommentId.toString commentId) + { original = c, request = CommentEvent.Deleting } + model.modifyCommentRequests + in + ( { model + | timeline = timeline + , modifyCommentRequests = modifyCommentRequests + } + , deleteTicketComment + appContext + projectRef + contribRef + commentId + ) + + Nothing -> + ( model, Cmd.none ) + + DeleteCommentFinished commentId res -> + let + timeline = + case ( res, Dict.get (CommentId.toString commentId) model.modifyCommentRequests ) of + ( Failure _, Just c ) -> + let + eventToRemoved evt = + case evt of + CommentRemoved d -> + if CommentId.equals d.id commentId then + Comment c.original + + else + evt + + _ -> + evt + in + model.timeline + |> RemoteData.map (List.map eventToRemoved) + + _ -> + model.timeline + + request r = + case res of + Success _ -> + CommentEvent.Deleted + + Failure err -> + CommentEvent.DeleteFailure err + + _ -> + r.request + + modifyCommentRequests = + Dict.update + (CommentId.toString commentId) + (Maybe.map (\r -> { r | request = request r })) + model.modifyCommentRequests + in + ( { model | timeline = timeline, modifyCommentRequests = modifyCommentRequests }, Cmd.none ) + + +addEvent : Model -> TicketEvent -> Model +addEvent model event = + let + timeline = + model.timeline + |> RemoteData.map (\tl -> tl ++ [ event ]) + in + { model | timeline = timeline } + + +isUpdatable : Model -> Bool +isUpdatable model = + RemoteData.isSuccess model.timeline + + + +-- EFFECTS + + +fetchTicketTimeline : AppContext -> ProjectRef -> TicketRef -> Cmd Msg +fetchTicketTimeline appContext projectRef contributionRef = + ShareApi.projectTicketTimeline projectRef contributionRef + |> HttpApi.toRequest + (Decode.field "items" (Decode.list TicketEvent.decode)) + (RemoteData.fromResult >> FetchTicketTimelineFinished) + |> HttpApi.perform appContext.api + + +postTicketComment : AppContext -> ProjectRef -> TicketRef -> String -> Cmd Msg +postTicketComment appContext projectRef contributionRef text = + ShareApi.createProjectTicketComment projectRef contributionRef text + |> HttpApi.toRequest CommentEvent.decodeCommentDetails (PostNewCommentFinished text) + |> HttpApi.perform appContext.api + + +updateTicketComment : AppContext -> ProjectRef -> TicketRef -> CommentId -> Int -> String -> Cmd Msg +updateTicketComment appContext projectRef contributionRef commentId originalRevision text = + ShareApi.updateProjectTicketComment projectRef contributionRef commentId originalRevision text + |> HttpApi.toRequest (Decode.field "comment" CommentEvent.decodeCommentDetails) (SaveEditCommentFinished commentId text) + |> HttpApi.perform appContext.api + + +deleteTicketComment : AppContext -> ProjectRef -> TicketRef -> CommentId -> Cmd Msg +deleteTicketComment appContext projectRef contributionRef commentId = + ShareApi.deleteProjectTicketComment projectRef contributionRef commentId + |> HttpApi.toRequestWithEmptyResponse + (RemoteData.fromResult >> DeleteCommentFinished commentId) + |> HttpApi.perform appContext.api + + + +-- VIEW + + +viewStatusChangeEvent : DateTimeContext a -> TicketEvent.StatusChangeDetails -> Html Msg +viewStatusChangeEvent dtContext ({ newStatus, oldStatus } as details) = + case newStatus of + Open -> + let + title = + case oldStatus of + Just Closed -> + "Re-opened" + + _ -> + "Opened" + in + StatusChangeEvent.view dtContext Icon.conversation title details + + Closed -> + StatusChangeEvent.view dtContext Icon.archive "Closed" details + + +viewTicketEvent : AppContext -> ProjectRef -> ModifyCommentRequests -> TicketEvent -> Html Msg +viewTicketEvent appContext projectRef modifyCommentRequests event = + let + actions commenter = + CommentEvent.commentEventActions appContext + { editMsg = EditComment + , updateEditingMsg = UpdateEditingComment + , saveEditMsg = SaveEditComment + , cancelEditMsg = CancelEditComment + , deleteMsg = ShowDeleteCommentConfirmation + , confirmDeleteMsg = DeleteComment + , cancelDeleteMsg = CancelDeleteComment + } + projectRef + commenter + in + case event of + TicketEvent.StatusChange details -> + TimelineEvent.view + [ viewStatusChangeEvent appContext details ] + + TicketEvent.Comment details -> + let + request = + modifyCommentRequests + |> Dict.get (CommentId.toString details.id) + |> Maybe.map .request + |> Maybe.withDefault CommentEvent.Idle + in + TimelineEvent.view + [ CommentEvent.viewCommentEvent + appContext + (actions details.actor.handle) + { details = details, request = request } + ] + + TicketEvent.CommentRemoved details -> + TimelineEvent.view + [ CommentEvent.viewRemovedCommentEvent + appContext + details + ] + + +view : AppContext -> ProjectRef -> Model -> Html Msg +view appContext projectRef model = + case model.timeline of + Success timeline -> + div [] + [ div [ class "timeline" ] (List.map (viewTicketEvent appContext projectRef model.modifyCommentRequests) timeline) + , CommentEvent.viewNewComment + appContext + { comment = model.newComment + , updateMsg = UpdateNewComment + , postMsg = PostNewComment + } + ] + + _ -> + div [] + [ Placeholder.text + |> Placeholder.view + ] diff --git a/src/UnisonShare/Timeline/CommentEvent.elm b/src/UnisonShare/Timeline/CommentEvent.elm new file mode 100644 index 00000000..9ec4d207 --- /dev/null +++ b/src/UnisonShare/Timeline/CommentEvent.elm @@ -0,0 +1,377 @@ +module UnisonShare.Timeline.CommentEvent exposing (..) + +import Html exposing (Html, div, footer, h2, header, span, text) +import Html.Attributes exposing (class, classList) +import Http +import Json.Decode as Decode +import Json.Decode.Extra exposing (when) +import Json.Decode.Pipeline exposing (required) +import Lib.UserHandle exposing (UserHandle) +import Markdown +import UI +import UI.Button as Button +import UI.ByAt as ByAt +import UI.Card as Card +import UI.DateTime as DateTime exposing (DateTime) +import UI.Divider as Divider +import UI.Form.TextField as TextField +import UI.Icon as Icon +import UI.StatusBanner as StatusBanner +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Session as Session +import UnisonShare.Timeline.CommentId as CommentId exposing (CommentId) +import UnisonShare.Timeline.TimelineEvent as TimelineEvent exposing (TimelineEventDetails) +import UnisonShare.User as User exposing (UserSummary) + + + +-- MODEL + + +type CommentRevision + = Initial + | Edited + { revisionNumber : Int + , editedAt : DateTime + , editedBy : UserSummary + } + + +type alias CommentDetails = + TimelineEventDetails + { id : CommentId + , content : String + , revision : Int + } + + +type alias RemovedCommentDetails = + { id : CommentId + , timestamp : DateTime + , deletedAt : DateTime + } + + +type ModifyCommentRequest + = Idle + | ConfirmDelete + | Deleting + | DeleteFailure Http.Error + | Deleted + | Editing String + | SavingEdit String + | EditFailure Http.Error + | EditSaved + + +type alias ModifiableComment = + { details : CommentDetails + , request : ModifyCommentRequest + } + + +type CommentEvent + = CommentEvent ModifiableComment + | RemovedCommentEvent RemovedCommentDetails + + + +-- VIEW + + +type alias ActionMsgs msg = + { editMsg : CommentId -> msg + , updateEditingMsg : CommentId -> String -> msg + , saveEditMsg : CommentId -> String -> msg + , cancelEditMsg : CommentId -> msg + , deleteMsg : CommentId -> msg + , confirmDeleteMsg : CommentId -> msg + , cancelDeleteMsg : CommentId -> msg + } + + +type CommentEventActions msg + = NoModifyAccess + | CanModify (ActionMsgs msg) + + +commentEventActions : AppContext -> ActionMsgs msg -> ProjectRef -> UserHandle -> CommentEventActions msg +commentEventActions appContext msgs projectRef commenterHandle = + let + isCommentOwner = + Session.isHandle commenterHandle appContext.session + + canEditAndDelete = + Session.hasProjectAccess projectRef appContext.session || isCommentOwner + in + if canEditAndDelete then + CanModify msgs + + else + NoModifyAccess + + +viewCommentEvent : AppContext -> CommentEventActions msg -> ModifiableComment -> Html msg +viewCommentEvent appContext actions { details, request } = + let + byAt = + ByAt.byAt details.actor details.timestamp + |> ByAt.view appContext.timeZone appContext.now + + md comment = + Card.card + [ Markdown.toHtml [ class "definition-doc" ] comment ] + |> Card.asContained + |> Card.withTightPadding + |> Card.withClassName "comment-event_content" + |> Card.view + + currentComment = + md details.content + + editAndDeleteActions editMsg deleteMsg = + div [ class "event-actions" ] + [ Button.icon (editMsg details.id) Icon.writingPad + |> Button.small + |> Button.subdued + |> Button.view + , Button.icon (deleteMsg details.id) Icon.trash + |> Button.small + |> Button.subdued + |> Button.view + ] + + ( content, status, actions_ ) = + case ( actions, request ) of + ( CanModify { confirmDeleteMsg, cancelDeleteMsg }, ConfirmDelete ) -> + ( currentComment + , UI.nothing + , div [ class "event-actions" ] + [ Button.icon (cancelDeleteMsg details.id) Icon.x + |> Button.small + |> Button.subdued + |> Button.view + , Button.button (confirmDeleteMsg details.id) "Delete" + |> Button.small + |> Button.emphasized + |> Button.view + ] + ) + + ( _, Deleting ) -> + ( currentComment, StatusBanner.working "Deleting...", UI.nothing ) + + ( CanModify { editMsg, deleteMsg }, DeleteFailure _ ) -> + ( currentComment + , StatusBanner.bad "Something happened on our end and the comment wasn't deleted." + , editAndDeleteActions editMsg deleteMsg + ) + + ( _, Deleted ) -> + ( UI.nothing, UI.nothing, UI.nothing ) + + ( CanModify { updateEditingMsg, cancelEditMsg, saveEditMsg }, Editing edit ) -> + ( div [ class "comment-event_edit" ] + [ TextField.fieldWithoutLabel + (updateEditingMsg details.id) + "Be kind and respectful." + edit + |> TextField.withRows 4 + |> TextField.withId ("edit_" ++ CommentId.toString details.id) + |> TextField.view + ] + , UI.nothing + , div [ class "event-actions" ] + [ Button.icon (cancelEditMsg details.id) Icon.x + |> Button.small + |> Button.subdued + |> Button.view + , Button.button (saveEditMsg details.id edit) "Save" + |> Button.small + |> Button.emphasized + |> Button.view + ] + ) + + ( _, SavingEdit edit ) -> + ( md edit, StatusBanner.working "Saving...", UI.nothing ) + + ( CanModify { editMsg, deleteMsg }, EditFailure _ ) -> + ( currentComment + , StatusBanner.bad "Something happened on our end and the comment wasn't saved." + , editAndDeleteActions editMsg deleteMsg + ) + + ( CanModify { editMsg, deleteMsg }, _ ) -> + ( currentComment, UI.nothing, editAndDeleteActions editMsg deleteMsg ) + + _ -> + ( currentComment, UI.nothing, UI.nothing ) + in + div [ class "comment-event" ] + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.speechBubbleFromRight + , byAt + , status + ] + , actions_ + ] + , content + ] + + +viewRemovedCommentEvent : AppContext -> RemovedCommentDetails -> Html msg +viewRemovedCommentEvent appContext { timestamp, deletedAt } = + div [ class "removed-comment-event" ] + [ header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] + [ TimelineEvent.viewIcon Icon.speechBubbleFromRightOutlined + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle "Comment removed" ] + , span [ class "by-at" ] + [ text "Posted " + , DateTime.view (DateTime.DistanceFrom appContext.now) appContext.timeZone timestamp + , span [] + [ text "and deleted " + , DateTime.view (DateTime.DistanceFrom appContext.now) appContext.timeZone deletedAt + ] + ] + ] + ] + ] + + +type NewComment + = WritingComment String + | PostingComment String + | CommentPostingFailure String Http.Error + | CommentPostingSuccess CommentDetails + + +type alias NewCommentConfig msg = + { updateMsg : String -> msg + , postMsg : msg + , comment : NewComment + } + + +viewNewComment : AppContext -> NewCommentConfig msg -> Html msg +viewNewComment appContext { updateMsg, postMsg, comment } = + let + viewField txt = + TextField.fieldWithoutLabel updateMsg + "Be kind and respectful." + txt + |> TextField.withRows 4 + |> TextField.view + + postButton = + Button.button postMsg "Post comment" + |> Button.emphasized + |> Button.view + in + case appContext.session of + Session.SignedIn _ -> + let + ( commentField, actions, working ) = + case comment of + WritingComment txt -> + ( viewField txt + , footer [ class "comment-actions" ] [ postButton ] + , False + ) + + PostingComment text -> + ( viewField text + , footer [ class "comment-actions" ] + [ StatusBanner.working "Posting..." + , postButton + ] + , True + ) + + CommentPostingSuccess _ -> + ( viewField "" + , footer [ class "comment-actions" ] + [ StatusBanner.good "New comment posted" + , postButton + ] + , False + ) + + CommentPostingFailure txt _ -> + ( viewField txt + , footer [ class "comment-actions" ] + [ StatusBanner.bad "Couldn't post comment, please try agan" + , postButton + ] + , False + ) + in + div [ class "new-comment_form", classList [ ( "new-comment_form_working", working ) ] ] + [ Divider.divider |> Divider.small |> Divider.withoutMargin |> Divider.view + , h2 [] [ text "Post a comment" ] + , commentField + , actions + ] + + _ -> + UI.nothing + + + +-- DECODE + + +{-| TODO +-} +decodeRevision : Decode.Decoder CommentRevision +decodeRevision = + let + makeRevisionDetails revNumber editedAt editedBy = + { revisionNumber = revNumber, editedAt = editedAt, editedBy = editedBy } + in + Decode.oneOf + [ when (Decode.field "revision" Decode.int) ((==) 0) (Decode.succeed Initial) + , Decode.map Edited + (Decode.succeed makeRevisionDetails + |> required "revision" Decode.int + |> required "editedAt" DateTime.decode + |> required "editedBy" User.decodeSummary + ) + ] + + +decodeCommentDetails : Decode.Decoder CommentDetails +decodeCommentDetails = + let + makeCommentDetails id content timestamp actor revision = + { id = id + , content = content + , timestamp = timestamp + , actor = actor + , revision = revision + } + in + Decode.succeed makeCommentDetails + |> required "id" CommentId.decode + |> required "content" Decode.string + |> required "timestamp" DateTime.decode + |> required "actor" User.decodeSummary + |> required "revision" Decode.int + + +decodeRemovedCommentDetails : Decode.Decoder RemovedCommentDetails +decodeRemovedCommentDetails = + let + makeCommentRemovedDetails id deletedAt timestamp = + { id = id + , deletedAt = deletedAt + , timestamp = timestamp + } + in + Decode.succeed makeCommentRemovedDetails + |> required "id" CommentId.decode + |> required "deletedAt" DateTime.decode + |> required "timestamp" DateTime.decode diff --git a/src/UnisonShare/Timeline/CommentId.elm b/src/UnisonShare/Timeline/CommentId.elm new file mode 100644 index 00000000..c62dcf53 --- /dev/null +++ b/src/UnisonShare/Timeline/CommentId.elm @@ -0,0 +1,27 @@ +module UnisonShare.Timeline.CommentId exposing (CommentId, decode, equals, fromString, toString) + +import Json.Decode as Decode + + +type CommentId + = CommentId String + + +fromString : String -> CommentId +fromString = + CommentId + + +toString : CommentId -> String +toString (CommentId commentId) = + commentId + + +equals : CommentId -> CommentId -> Bool +equals (CommentId a) (CommentId b) = + a == b + + +decode : Decode.Decoder CommentId +decode = + Decode.map CommentId Decode.string diff --git a/src/UnisonShare/Timeline/StatusChangeEvent.elm b/src/UnisonShare/Timeline/StatusChangeEvent.elm new file mode 100644 index 00000000..9ed9aef5 --- /dev/null +++ b/src/UnisonShare/Timeline/StatusChangeEvent.elm @@ -0,0 +1,24 @@ +module UnisonShare.Timeline.StatusChangeEvent exposing (..) + +import Html exposing (Html, div) +import Html.Attributes exposing (class) +import UI.ByAt as ByAt +import UI.Icon exposing (Icon) +import UnisonShare.DateTimeContext exposing (DateTimeContext) +import UnisonShare.Timeline.TimelineEvent as TimelineEvent exposing (TimelineEventDetails) + + +view : DateTimeContext a -> Icon msg -> String -> TimelineEventDetails d -> Html msg +view dtContext icon title { actor, timestamp } = + let + byAt = + ByAt.byAt actor timestamp + |> ByAt.view dtContext.timeZone dtContext.now + in + div [ class "timeline-event_status-change" ] + [ TimelineEvent.viewHeader + [ TimelineEvent.viewIcon icon + , TimelineEvent.viewDescription [ TimelineEvent.viewTitle title ] + , byAt + ] + ] diff --git a/src/UnisonShare/Timeline/TimelineEvent.elm b/src/UnisonShare/Timeline/TimelineEvent.elm new file mode 100644 index 00000000..9fd89f59 --- /dev/null +++ b/src/UnisonShare/Timeline/TimelineEvent.elm @@ -0,0 +1,41 @@ +module UnisonShare.Timeline.TimelineEvent exposing (..) + +import Html exposing (Html, div, header, strong, text) +import Html.Attributes exposing (class) +import UI.DateTime exposing (DateTime) +import UI.Icon as Icon +import UnisonShare.User exposing (UserSummary) + + +type alias TimelineEventDetails d = + { d | timestamp : DateTime, actor : UserSummary } + + + +-- VIEW + + +viewHeader : List (Html msg) -> Html msg +viewHeader headerDescription = + header [ class "timeline-event_header" ] + [ div [ class "timeline-event_header_description" ] headerDescription ] + + +viewIcon : Icon.Icon msg -> Html msg +viewIcon ico = + div [ class "timeline-event_icon" ] [ Icon.view ico ] + + +viewTitle : String -> Html msg +viewTitle title = + strong [ class "timeline-event_title" ] [ text title ] + + +viewDescription : List (Html msg) -> Html msg +viewDescription description = + div [ class "timeline-event_description" ] description + + +view : List (Html msg) -> Html msg +view content = + div [ class "timeline-event" ] content diff --git a/src/UnisonShare/Tour.elm b/src/UnisonShare/Tour.elm new file mode 100644 index 00000000..45255b58 --- /dev/null +++ b/src/UnisonShare/Tour.elm @@ -0,0 +1,25 @@ +module UnisonShare.Tour exposing (..) + +import Json.Decode as Decode + + +type Tour + = WelcomeTerms + + + +-- HELPERS + + +toString : Tour -> String +toString _ = + "welcome-terms" + + + +-- DECODE + + +decode : Decode.Decoder Tour +decode = + Decode.succeed WelcomeTerms diff --git a/src/UnisonShare/UnisonRelease.elm b/src/UnisonShare/UnisonRelease.elm new file mode 100644 index 00000000..d6739a74 --- /dev/null +++ b/src/UnisonShare/UnisonRelease.elm @@ -0,0 +1,84 @@ +module UnisonShare.UnisonRelease exposing (..) + + +type alias UnisonRelease = + { name : String + , tag : String + } + + +latest : UnisonRelease +latest = + m5 + + +all : List UnisonRelease +all = + latest :: past + + +past : List UnisonRelease +past = + [ m4, m3, m2, m1 ] + + + +-- INSTALL INSTRUCTIONS + + +installForMac : UnisonRelease -> String +installForMac _ = + "brew install unisonweb/unison/unison-language" + + +installForLinux : UnisonRelease -> List String +installForLinux release = + [ "mkdir unisonlanguage" + , "curl -L " ++ linuxUrl release ++ " --output unisonlanguage/ucm.tar.gz" + , "tar -xzf unisonlanguage/ucm.tar.gz -C unisonlanguage" + , "./unisonlanguage/ucm" + ] + + +macUrl : UnisonRelease -> String +macUrl release = + "https://github.com/unisonweb/unison/releases/download/" ++ release.tag ++ "/ucm-macos.tar.gz" + + +linuxUrl : UnisonRelease -> String +linuxUrl release = + "https://github.com/unisonweb/unison/releases/download/" ++ release.tag ++ "/ucm-linux.tar.gz" + + +windowsUrl : UnisonRelease -> String +windowsUrl release = + "https://github.com/unisonweb/unison/releases/download/" ++ release.tag ++ "/ucm-windows.zip" + + + +-- RELEASES + + +m5 : UnisonRelease +m5 = + { name = "M5", tag = "release%2FM5" } + + +m4 : UnisonRelease +m4 = + { name = "M4", tag = "release%2FM4" } + + +m3 : UnisonRelease +m3 = + { name = "M3", tag = "release%2FM3" } + + +m2 : UnisonRelease +m2 = + { name = "M2", tag = "release%2FM2" } + + +m1 : UnisonRelease +m1 = + { name = "M2", tag = "release%2FM1" } diff --git a/src/UnisonShare/User.elm b/src/UnisonShare/User.elm new file mode 100644 index 00000000..f059eebc --- /dev/null +++ b/src/UnisonShare/User.elm @@ -0,0 +1,99 @@ +module UnisonShare.User exposing + ( User + , UserDetails + , UserSummary + , decodeDetails + , decodeSummary + , name + , toAvatar + ) + +import Json.Decode as Decode exposing (field, maybe, nullable, string) +import Json.Decode.Pipeline exposing (required) +import Lib.UserHandle as UserHandle exposing (UserHandle) +import Lib.Util exposing (decodeUrl) +import UI.Avatar as Avatar exposing (Avatar) +import UI.Icon as Icon +import Url exposing (Url) + + +type alias User u = + { u + | handle : UserHandle + , name : Maybe String + , avatarUrl : Maybe Url + } + + +type alias UserSummary = + User { pronouns : Maybe String } + + +type alias UserDetails = + User + { website : Maybe Url + , location : Maybe String + , bio : Maybe String + , pronouns : Maybe String + , twitterHandle : Maybe String + } + + + +-- HELPERS + + +name : User u -> String +name user = + Maybe.withDefault (UserHandle.toString user.handle) user.name + + +toAvatar : User u -> Avatar msg +toAvatar user = + Avatar.avatar user.avatarUrl (Just (name user)) + |> Avatar.withIcon Icon.user + + + +-- DECODE + + +decodeSummary : Decode.Decoder UserSummary +decodeSummary = + let + makeSummary handle name_ avatarUrl = + { handle = handle + , name = name_ + , avatarUrl = avatarUrl + , pronouns = Nothing + } + in + Decode.map3 makeSummary + (field "handle" UserHandle.decodeUnprefixed) + (maybe (field "name" string)) + (maybe (field "avatarUrl" decodeUrl)) + + +decodeDetails : Decode.Decoder UserDetails +decodeDetails = + let + makeDetails handle name_ avatarUrl pronouns bio location website twitterHandle = + { handle = handle + , name = name_ + , avatarUrl = avatarUrl + , pronouns = pronouns + , bio = bio + , location = location + , website = website + , twitterHandle = twitterHandle + } + in + Decode.succeed makeDetails + |> required "handle" UserHandle.decodeUnprefixed + |> required "name" (nullable string) + |> required "avatarUrl" (nullable decodeUrl) + |> required "pronouns" (nullable string) + |> required "bio" (nullable string) + |> required "location" (nullable string) + |> required "website" (nullable decodeUrl) + |> required "twitterHandle" (nullable string) diff --git a/src/UnisonShare/UserPageHeader.elm b/src/UnisonShare/UserPageHeader.elm new file mode 100644 index 00000000..8fbe775b --- /dev/null +++ b/src/UnisonShare/UserPageHeader.elm @@ -0,0 +1,85 @@ +module UnisonShare.UserPageHeader exposing (..) + +import Lib.UserHandle exposing (UserHandle) +import UI.Icon as Icon +import UI.Navigation as Nav +import UI.PageHeader as PageHeader exposing (PageHeader) +import UI.ProfileSnippet as ProfileSnippet +import UnisonShare.Link as Link +import UnisonShare.User exposing (User) + + +type ActiveNavItem + = UserProfile + | Code + | Contributions + + + +-- CREATE + + +empty : PageHeader msg +empty = + let + context = + { isActive = False + , click = Nothing + , content = ProfileSnippet.loading |> ProfileSnippet.view + } + in + PageHeader.pageHeader context + + +loading : PageHeader msg +loading = + empty + + +error : PageHeader msg +error = + empty + + +view : msg -> Bool -> ActiveNavItem -> UserHandle -> User u -> PageHeader msg +view toggleMobileNavMsg mobileNavIsOpen activeNavItem handle user = + let + context_ = + ProfileSnippet.profileSnippet user |> ProfileSnippet.view + + context = + { isActive = activeNavItem == UserProfile + , click = Just (Link.userProfile handle) + , content = context_ + } + + nav = + if activeNavItem == Code then + Nav.withItems + [] + (Nav.navItem "Code" (Link.userCodeRoot handle) |> Nav.navItemWithIcon Icon.ability) + [ Nav.navItem "Contributions" (Link.userContributions handle) |> Nav.navItemWithIcon Icon.branch ] + Nav.empty + + else if activeNavItem == Contributions then + Nav.withItems + [ Nav.navItem "Code" (Link.userCodeRoot handle) |> Nav.navItemWithIcon Icon.ability ] + (Nav.navItem "Contributions" (Link.userContributions handle) |> Nav.navItemWithIcon Icon.branch) + [] + Nav.empty + + else + Nav.withNoSelectedItems + [ Nav.navItem "Code" (Link.userCodeRoot handle) + |> Nav.navItemWithIcon Icon.ability + , Nav.navItem "Contributions" (Link.userContributions handle) |> Nav.navItemWithIcon Icon.branch + ] + Nav.empty + in + context + |> PageHeader.pageHeader + |> PageHeader.withNavigation + { navigation = nav + , mobileNavToggleMsg = toggleMobileNavMsg + , mobileNavIsOpen = mobileNavIsOpen + } diff --git a/src/WebsiteApi.elm b/src/WebsiteApi.elm new file mode 100644 index 00000000..1d7e1213 --- /dev/null +++ b/src/WebsiteApi.elm @@ -0,0 +1,8 @@ +module WebsiteApi exposing (..) + +import Lib.HttpApi exposing (Endpoint(..)) + + +whatsNewFeed : Endpoint +whatsNewFeed = + GET { path = [ "feed.json" ], queryParams = [] } diff --git a/src/WhatsNew.elm b/src/WhatsNew.elm new file mode 100644 index 00000000..a23cd613 --- /dev/null +++ b/src/WhatsNew.elm @@ -0,0 +1,143 @@ +port module WhatsNew exposing (..) + +import Json.Decode as Decode exposing (field, string) +import Json.Encode as Encode +import Lib.HttpApi as HttpApi exposing (HttpResult) +import List.Extra as ListE +import UI.DateTime as DateTime exposing (DateTime) +import UnisonShare.AppContext exposing (AppContext) +import WebsiteApi + + +type PostId + = PostId String + + +type alias Post = + { id : PostId + , title : String + , summary : String + , url : String + , publishedAt : DateTime + } + + +type alias ReadPostIds = + List PostId + + +type alias LoadedWhatsNew = + { posts : List Post + , readPostIds : ReadPostIds + } + + +type WhatsNew + = Loading ReadPostIds + | Success LoadedWhatsNew + | Failure ReadPostIds + + +isUnread : LoadedWhatsNew -> Post -> Bool +isUnread whatsNew post = + isUnread_ whatsNew.readPostIds post.id + + +isUnread_ : ReadPostIds -> PostId -> Bool +isUnread_ readPostIds postId = + not (List.member postId readPostIds) + + +hasAnyUnreadPosts : WhatsNew -> Bool +hasAnyUnreadPosts whatsNew = + case whatsNew of + Success wn -> + List.any (.id >> isUnread_ wn.readPostIds) wn.posts + + _ -> + False + + + +-- DECODE + + +decodePost : Decode.Decoder Post +decodePost = + Decode.map5 + Post + (Decode.map PostId (field "id" string)) + (field "title" string) + (field "summary" string) + (field "url" string) + (field "date_published" DateTime.decode) + + +decode : WhatsNew -> Decode.Decoder LoadedWhatsNew +decode whatsNew = + let + readPostIds = + case whatsNew of + Loading readPostIds_ -> + readPostIds_ + + Success d -> + d.readPostIds + + Failure readPostIds_ -> + readPostIds_ + in + Decode.map (\p -> LoadedWhatsNew p readPostIds) (field "items" (Decode.list decodePost)) + + +postIdToString : PostId -> String +postIdToString (PostId postId) = + postId + + + +-- EFFECTS + + +fetchFeed : AppContext -> WhatsNew -> (HttpResult LoadedWhatsNew -> msg) -> Cmd msg +fetchFeed appContext whatsNew msg = + WebsiteApi.whatsNewFeed + |> HttpApi.toRequest (decode whatsNew) msg + |> HttpApi.perform appContext.websiteApi + + +markAllAsRead : WhatsNew -> ( WhatsNew, Cmd msg ) +markAllAsRead whatsNew = + case whatsNew of + Success wn -> + let + allReadPostIds = + (List.map .id wn.posts + ++ wn.readPostIds + ) + |> ListE.uniqueBy postIdToString + in + ( Success { wn | readPostIds = allReadPostIds } + , allReadPostIds + |> encode + |> updateWhatsNewReadPostIds + ) + + _ -> + ( whatsNew, Cmd.none ) + + + +-- PORTS + + +encode : List PostId -> Encode.Value +encode postIds = + let + encodePostId (PostId p) = + Encode.string p + in + Encode.list encodePostId postIds + + +port updateWhatsNewReadPostIds : Encode.Value -> Cmd msg diff --git a/src/assets/circle-grid-color.svg b/src/assets/circle-grid-color.svg new file mode 100644 index 00000000..ecc08bfc --- /dev/null +++ b/src/assets/circle-grid-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/confetti.svg b/src/assets/confetti.svg new file mode 100644 index 00000000..6d2fcd51 --- /dev/null +++ b/src/assets/confetti.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/dev-favicon.svg b/src/assets/dev-favicon.svg new file mode 100644 index 00000000..203a0ad0 --- /dev/null +++ b/src/assets/dev-favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/favicon.svg b/src/assets/favicon.svg new file mode 100644 index 00000000..c7649b7a --- /dev/null +++ b/src/assets/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/unison-cloud-splash.svg b/src/assets/unison-cloud-splash.svg new file mode 100644 index 00000000..b927e62a --- /dev/null +++ b/src/assets/unison-cloud-splash.svg @@ -0,0 +1,1399 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/unison-logo-circle.png b/src/assets/unison-logo-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..a06340e0048cdd2c15ac3cfb9ab479879d2ff572 GIT binary patch literal 42547 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 Ah5!Hn literal 0 HcmV?d00001 diff --git a/src/assets/unison-logo-square.png b/src/assets/unison-logo-square.png new file mode 100644 index 0000000000000000000000000000000000000000..35df1bd9cb7ef5fcbca95d0ddb6ab996f3367a67 GIT binary patch literal 31880 zcmeEtRajJS^zP6&v@n3AVgN&{fOJVn#|#V|N=Pf+FbD{Upwit814wtb(%mW29Rt#5 z^ZP&N;#{8l{{_!H?ETgL_WIU(*Sp>ws;(+e0;U6lKp>Jg3a>RmARH+0_nZ(PxPnyX z=L3H3IV$Knfk2O5-Th(7#`2i}7qOf)<)uMC2N*Vie{e0ORHQ(l(nz8!Q#=sJoAJ$S zDVRIf?!2EZ+%k3joRz%Aqz4zd%YC2KwX7tj>f19t=}M~XPL%?suzVwaj)40v#bH;a zKdh{ZJUJBQvgWfX`>miFdW|FMitleeE7dm!DJX@GVQ0T5dBla6Po%j8?S#d&JrQ`N z(j!0=P}YifZ|9uB>ATI_72~so{WFh&%cr#}nwz*k_al#m7MTYP>wOOcldyoLg05<6 zpWOZWzrcmu{qB*&?tW@y)bDODkxBs%0P+|4-<$ut3IA&e|6j*pjuG_md4!Vor>-21 zBV$1x;}?f#u>zmq)i(Gs)xY_yW`-$`=2ZW+{S_W_c7&$?GP& zn!`iHohLUqB7hH)Boxd?f#TsBeA6=(p(dI*KcGBClzwUfIfUF5;aH%a&*|7EdMOch z83mcv$O|+vH@YU7j**y^+b`O0@)YpIn)2#cQ3ZvG&c)k`_8Vdr7rM5F<$SmI`zui+ z-w9~YTp<6~d|BAh2nyA@N&5jFTz+KmJI^jwW7yV)%&~Bq>b@ zmDIIb`&9Mwh)udTTCiwnGXpjr>t~yDV|NPcH?7+)MKA%Bjf~u zWPs`L`_fvl1Mom5!0x!qNd^#rIrsCQ?npB`af19kAE7`@6zWSF6bMpMMluj+j8+Qo z;FCX+cIZAn2!(?LFUg+G{4A+=p_e)Oj1LT>4vg7CXA?{zf=vWU$G2Z#1jSqFRz2IX z@k06|H}b(X#YL<{^JI>WPeAF`C=|#=yDtOPPozM>I*$WF`-g$m)kG#a(nSZ-MZf3D zyn(Z4GZKSJXkjpa;SDRRdtQP&>HS1GS?4iv;^LAncpzS5;!&K+*GV+I%R|2imX{7L zmwV#w47d-g$XK$-Dk%>Ci>@H?C(#lI!}4#gyr7n33Yb4s3t~RFZ`Vs^hkL+Z6^I2| z3rxp`QNW{ThGlnSc)HsUDtk>iZ++Pv9@90BE57`=yB)sJV?3B$`^R#u##t6oS@EcF zpt<;$pRb<#!FX}`!EfwOTc%S?AYPh26bLQ}F+Zew4VAYFx z4pdgc+Est0^29EGI*u+3(UtHPKF0_3`I~y0l%g{GBGHfXy6}%5j-=uJc%c(A_vu^b zkVQ3114=Jlz1_*o{={HP!u^StQ8Eb#G$*Z&MZK~~B7(}C3y}WZ^3Rb;K{X;Z1r;B? zG2pRMWUaKmRVCWM);AuUZPpIqbYnQ1M(Ji zj|B75m^)xJ>AxaFn^1{kk!@kp;cZH~GDJltAay-lh>1X*!rbKP>rK5zCF|3{{!I+`2IZn<0QvlZd{ScMz zt*I~37bQxq6macD@A~A0vl;@+{|V5Rn>~mM8;n+oiGfYfQEG!8dg4N&<)M#c3HYts zhfTH1GUY1J`b5vn{EEndyVk&68Q|`^tQCZVfIo9WGr?E`;o-8h%gZay00L<|LOt|h zPZlUXAs*FF36wf!)`2Y?i6q+Hgubxc1RcT)op({*RU}@KH~Q%_}Vmv&PsSfA}h$9@^A6T zkNXm1Kz24}cxzTa^Q$P_+R(tpNNU>+V{`oiMtM=WKUwJ9gWuaumdCM< zt6d#f1t1VJA@p-BxBuxo8fm^EJcs>Sx8wT$_yj2PE{Fax@5sW?$L&cKCsO8PK8lae zZW=pnwbqGm3w?dtC%M-Ha{av7$av~#*Oyun*m0rJ+g_ZkGaQJ`^}cN?khjS;P3G7) z!;Yq=_YR+>nshfP@z?S++0L)W$=*ryPI#-Pi(R5+ALduya8?i-F!t!59~n~`*=J4K zDED$V-D>bi$b(AUexxHeIx2z@G(U?|Jd&dBKSTbW;7>YI47CU|BYx_nQ4zj*FmNN< zLX6*0*%n{f30lII!uv2>h=j898SF&+Fv&_3beBnvjQ^7yX?-UE27m@Z`O=ZbO=BNT zbK;{JH$OG1ULGp#CciqtG#zYE{6o}3ZXVGZj<9fslt|64@pdrl{I)iegO) z+7>`6Yfj|mc041T3dBwv-Q3=^r+%J5x}-e!E}m(4D7ctbx+h5&vg-JbV37-d`JL~} z$SfYagb~9_$~e_H?urH%jt(y}4o{=VcMx8I04#qJDD*Ru8)c{RjuK72)jc zCSSxjW#ycUmRP=sSl*Z%>a--9=X+%nLh!L@2xR`#(4$z8f88Tgi(r3_yZ9L2YE-6w zrchWaMjw^>D!tX7>%9VhNr&S^PsTSg>3z%9`M(4p7diC+W9xIzm2Z|vo%~=b>@Wvw zqEWe8szJ-p<-!;pf(7nQ20G9yNub%~o*&C%-HJxoBiUAU2xoDm&@(NLk>z97A4zWy znY80|u>PC9__qGo&&fkxqB2TaGBhbQL>Tchr~Yc8Jq)eKW3i6~@=vU*J^5yNyHSk2 z^hE&6qrp*?JsZ_&qtna$mp)E_e}RUaHR#;#O!R&o9rG_8`pEguw@+N)T&fnWcd-%C z)Uo)%7_9DQzNlr}i~~|5ggS0U?)$v7)vJ8u{#{Xl9lOU^{2%M`u!HE|BWd#Eyl~5Y z>U?PdN0(O@Etw@p$BU;S-R0zKheBu)cX06I3V$rge$IRTGP@*Ptv~)GM^DLw4M=(T zj4s?Et-zyD&8M15Y|dMk=o}8|zKiaVb|q%!U&ek+`E(C1-x7mTH8cX077d%9&XwD` z9HbMn-yYQyJ<_i0y^+5BS+agc$Y!1)a$|k3{=1kTLD!1d=aXZeuCii|b6xR;jmnCv zpY`p7@|QS34uC?n>sfFC*z z!eYvw|5^TRk{UDyoTC~X#uA!sft-eZo5yxWgdY5vRs`YW07Uo{IK4gmSg!??E?{+y zRGKkyHg0ch{Vp9wIuRZbyn1_;G~3?XbSq-ITh;}lg2A_q>V=XUoP~Q#FSXuU9uMu> z4dZhG2!S33Tl$VMwWf4)SRrC(B;h(QX;6(J$jx#si7()=#@ePAqi@K-BL<-()3M_& zib?S}66FtP$oyDE${jPK{$Qd+|7FHXR^oTPsy~`Zs5Z1;%DQVE4zzZjO6pY9G`!ki zVS83Ljb|TP;GywMzhyQCARbZSkem6NE^vcfI~YAc(URuf1oz zR87sLT2~Wl&TVcp+SQQrNxOLVeF_Eu%f~|rw;29w%zoQOoN*fD`SVrFLh2NFHB)cw zs!DZJ@o#2hLz68Jozrf;-8e87-_YP5CSuVE^I|lDx;O-eEs1M?A4VRe_bDb`I&4kj zw8EaqT##2hI)tKfd}BSbc5s!CF+mngxoZc-2f3+f?L-P?;J88z+IqkS~2|DRz$O*y?MvHAFWSp zeM<#TbJ9k~Tx!xLF3}`vvy+-XoBn3ptl(%EIC7wNoGVxmgYz`y^eiBMkH>&-2ea2X z?iwtMu(n}`GCFI4+GAkv?bJ(vov_SN_Tz9DvI?~BSMU9a~& z?p426&gPzx<|Hp;^KX4vO_6a1%Y>T{>R%cyt!OTJe(rEtOP+*dsNBtffaxAG zXu~?S>QnM2Cw)lHg72KYs*x~bmpyMeHs}SA2M1ToZ%f!;9dF2hvCI$6NI8M{q1Pgs zQp7TYD|lFA2)=W&SRh9cB-kVyA?5Ao>c(#&%q$&Zd0!p!?X1TOq1!q$c~3w{zv(u|9|wfW9Vw{rr~vKDsTxWS;qRhxOVH2mLznv`Jw2v4{b) zY1hI15WkyUR&F44iA8cyz4B%dXhZ7Csydke#i3S+n=yRNB;YX{%&*a{e{PRPGhIK9H{~KLqme+!rrz zDptv)zl^Qr2vK=%ZL87zWGx4WpShNpol094x))TuA5E-oy~u~+gp-!QCC!(ZBz1wr zMR-JVKh4L<>$8f6?0E>0L|%Wd`Y`EQV_oLrq+heO47K(JQgC5FJ0X-wrpRw$y5Xs< z-3-;9{s)KCnM)Rl>D0-G{$)$=JkwPxg8xOxr`{{ofUFi}oV`E(ChEhewl1uTgP*dvA_K%t96=}&&t9(QiLHV9#hd9ZeKC) z>k%D4wTxho2Ov3s$PwGfM7=dv`W)2u`=<5rc_;X46z{B;?3|=+@fI~X-}%!5ZuHxp z$;3Vk`W2~k`)^1G!YEDbPY1AJmANFwVgsZ}^_2716NZi*mJX)|$F3zA8_3g~NV30f zTVfu|t%m1I64ZGTL|o`5ww7c%F%b$Y1PEku2u#hjnW;e&$v4C}y5O|GHk(LZJYO{1 zmO9ZLC|Kw3bNBQ)5P!1`vV}30CJwR05#GS!NC4 zh-{E*++>PIhnk(r}dUftnRo0dr$BTM4w`Z_(QUs4Zx&_P3{5x}4Es zgP#34lL z=BoO7d3JimSq)c^S~GJgvt0>d;7n+*qdh<3gqexpaaY5RdJJ$YxkT-LKDTa2H{mb) z;{nyR;R(D!F=u7!2OEOIa|WNbrJZ-@BhFi6Qm?!#77bAx^p#NYi-?dmw>_Vi#`W{V zL*EAnA{-Zp?l^|@u$OU1ab!24m*I?drx$~_N5%o!=bdPZDD)Kw{b+bJLt?j*Q~+jnzkM@ODcwNYCdVA|N1G`O}Yk&HCJ=F=SF>W z+_?7tctoY`eCXll+UjvNQjBC3ZaGlYHq?khO2lLYo$@~P0VV@GA66F^b-VP^Ji`0K zUN|@DSX|F#ax@&{G=1CH>*G?f1D$QB(%S=5&bGfQ##0~wUIO+$moZ)PdW8p7#cqGE ze`7b7NoM!wlKW&q)bQ4d)0(UdelKw!DI3}-5I__uc<`?Vq%Oq_ycq_YrKhW>$8wOF z%F@L5?$?Pd)~%A5K2=4JL5fUay8bnQ3y>XY{c5D&icaTrm^_~s`{R5D9tZKzXKB|SQ@g4}lUfH_Z3CAN zJN+FGa6|I`J$onb-IsLSo%UzVRj49iJw6VDOdi5YtgKiLssO@TA+`waz)- z9kYFvHoP@RrT0ygY-l?N7WP*!~!=sUOf(Dl2&2xFprT@|GMm*#nicz`9 zBsI+v)YNrYj|v;;sw!bE;AZ=xJh{K1d-Z2RZtN|Mc%KBDY6M9z?dI3yws?6x8Sj5r z1X%HrGSg1EjTxOM9!1nC`#%@&lUfX#7E@_gNI5(yDBQ94nZqc4R8|`-_q$yBd0?jf z2xo~N0^tnfo&q3R46`#z(2uc;pQdxq>-v@qKz8<`ENjLSzb5|^3)&)j3 zXrnFUSrg>!K7ytE{kV_DC$scELMVc~QuJzCuv%x;Oz!q;k&VFW_T#rTug-f3ZaP+T zivdTrAbf^}s43Nd@ho|)2Cu^L8dKEcxX|&kzd%}v0;{}7F#rUUOb4c{i zBksAn43Rh|i9ADnWa=cI;+Tk;AS1&AzsYm2Rv6l9k6HA7s>nP&K)T{}ESKi0VEG&4 zLi)llO3%g|pB7t4h!(S9YQ7?96dXtznfUkb{R8nA&nA}Dt9am33%j{mK(p5^EjLh&&5%imnN46*d%<%w z`PwsEGWWp&2N`@vyu{gG(vd=&2J_mJ^{N+3C`;bN2x>z2tU$(Ek#N1F_<%AIe+@w{ z&8(iD0vv~s+tsmK4BVWuCv_#Jy);|0T*1Qn zIrAo%y$+S|pF8IZG@4mL4;#ORPnHc>ZS_H`FRXPJsAlL9F2w0`z_Kar{7GdKnb6w)3?a89rd8p|rEL zV>3PyeDw@Wt(G(c`uAGBwXkBh=TC>Zk3zu{hxvsHmx$sBUgE!P$lx}9gacne&KXup zsWa-`3te>0-`?k4Z4L0?__fs^eqt2P&lZd^3r1IUg{>iHS}i0XkiyHzBrwttFdRK5P9Y#{$;f8;i@J~-p+2u>Z6+vK&xHx z^YRWbIuc!1GoAGiU7CHBOQSqz0cB|~2eD*>nkZn7@)OL$k0*E01Y-S7!5jIxEjaxu zR@(0(hp|uq=%X;Y`YY*A*2xgj6!w4xZd5@m8$$#CD^`4)6GMFvk&YDE;+<>AA(QMS za+>EqMOuSJg=}eI&XY}F0x^8-RdMj2hT4wPO+S0b3yCZ>ux{>Vj6G(@S&?ZCgT6`K z!`4&@VC_6-)($D#>qkRpB;|+@47vC;K0lo;Gdjy=GS5Z>W%ddFXQ{iv$Ltb~&ccXI zSAo}R{CEot^+T2y!X5x^{qJVesLEd!Wd)&xBdUO!lupSuL=W)%nLgy7+rESPB-{VS z!tr`zzNLH(-VhSRV}h)s5yM`iqlSxVv%}E3H1-(MgHUtmH8bg+-zrx6(-WJIb&bjSS~!v z22nghnaIUTb$%4@)S8WJqhm%NC=L#m@^q_&Wbqeg5~)x0<>pwCjb5x2sU#gqUH!xx zP~H;q+_)NS1Bj;MBoojdDfQMb!o*}pWyxwE#!p=`ctEMTKcU=KMys8Xer=9E$gob5 z3(Xuax9t@trZyhz9|qvQ-dqSI>zqo>&baW295JZ~A+&D$XUWk>H$jsyoN;s5 z%go#+e&GEu_!>Sv*v$L=`UO~!wJgaH-KQmm^#^d^6(_9@9<2)wV;8IWiP9WIX6Zyy zB;#%mnq5zPF-Prc_q7!>KRVI+JC|aJMy(x63UX14bJnY<)mlES;nN@lgX{A?qfZb! zPFzq&WIFP_ZmNOg2}!4uN9=ejNRl+7RLz{E7t2*2d$agv?Zl6^6QUb^~v5vc;HBt z`0TFf3OcL_R>}=pNL{_k!28XDS`vu$nSU(jP!(Hbua#1BvWQKd^&?KLo)|jPXz$eI zn~do^KoPQuZCCf5%Vi*yl};NboGyV<>^)9}`!9f%>^>A*&pU}oxhMHWlw?dzp2pQd z^hkMX^uis~FeKHUn5VhwkTFjT=FodM+*ri{Vt;fuCxIBL0pmUD2HPvmS(+mD&oO!$ zC#xykgp2L`mF-7QpabXcFtb4VK2N*6wrm$AvMkSfQ0AzjtSizYhvk74Yp8_(a^-v6U;swP~hi4CdDJ%-FTK5O)o9Rq0ESwb}qE#FJDJ zI)7RU7;ot6X~8#}IGM)$kjdUI9{kO3VK455*f$zGFA9^0Rs=ah3qGO8cRN-ikCI4m zY5HChi0FAQY&faz8kZK55JRFZz*2aG2duX#n(x3J8v)Ez%W-{Gh z`vl>Wr6cxCWj^LWZA=634)=Q>K*CKw@{C7gq1hEaBde?Ybeqx;8XWmexi*IYv;S5L zH&{`Y?VG2V{lzu0Wz3Ff%{yaHGVNKWn2ebcQRUh%#j@D}>DYpDK$XB~p`ja<`;brP zXz~S(d<-2>p;=nzCB2 zpM4Yuh~WGT#rWJ!i;q-@3gxA-DHDIS1||v);FXusox%7SVM;t<@DNMk8T!G(lJSW0q593^w-{AqZg zk+TXA(D%5i5y5x^o6?1VH|ppO@@g#nScb9L!KZOiAc8<(12yD&$Z1a#7tWGIT3|Sn z`vpX91rt0xvu>|s@bZ_1A{1jXR)KTQDy{U}LhnQ~Sc$)K+9{V4aAE*?;Px=^eRlP|OZ058&j=pu>*{)#l^_TiYGQO2KOEEak703w4z z91vJmAAt_YIhz}9m@m?%`UFeBHUp~CAP_ekKNd(-qP{N-T2pF&1t_yX@+fR1AWd{s z36SM7y{o2Fa2}Ux`<9L!HQbIq)?Hn3N5u-3`vZZ~5v1A5D+U66mgaL#8;RvDvhv;k z6SBM-l4@`;ZItMx0m(_D-1tU|1RETb1@t20WLU8tQiD0@8YBR=uZ}o(GBJ1|?HUu= z1_%Mp(^M8Mv!(|E4woGd1iN*mzP!6TRSTS4+{r?txC%sA43)X-Qvl^IJ$S*j46r0m zGz^74oIBs-40fCbPjG&zoIlm0`-OgnzS9?!E-?KDhyjql)u!~#9dQ&QCr(HZvPhQBuJHG)H03mI|_OGDY{!+eIMitR1}coK^K4+r4`WHXtqCm(J*|u zL$q6Og@?legoh`wFX*Xh1l|gI!(g=kURyg8&>tuincVqHUXs^lr2RzdpMGXD)Ibgwwzq;)hai$?UI(f-(gs8LJ1F8x*)b{@_a(IFI)fXS3)Mi$zSk-WeuClX&Z~FTb z*7}S|%hw#mR$T9YHQQvF92)MI*1ejP6Y$APJivfg7M$eU|C5R|-R;2V%`b}A`mz9P z9!S%loP&H_lIU(v^%_W=*G#h|h@LewhG=Xf&}1 zf?zRizHoRFs01`$c!4=~b>W3hF8Uj`-svyx^6AQG+Q9#fgD|R@En;i>`zJ6yTqIjM zgWgz2S=Bt_HCCq#Z$dar14I2k)0;`zJJ8JG%%6K*4o3_({Jl|z+VV3cr`ti+^ zExa>wn4Uc*;zb@Z-bJ7)=(*M_A=gqKvS$Fa4!2)9N`GF160^1$&Goj?Mu)Jv-uV{R z!vBWo?|8q9X?7GWK)tH#%?}qvg3YwMq&$Nt`|BC6`VZf={Z>A))H2B@=3>{@3oAKV zvy?aFGQ{c$274v8dw{r^y$WhBhScGD->Od=aWNXsj!vqjQB**Z3rKq_CN5$5yoGkyPy6iAEU&) zLk)kLPN!*Xm!w0+eL|sbp#jr{8BSk&ManNyclCiyLhgXZfC8dIJm1H3X)EeY{j76S z3LO4*_$G+Z}^!e7`HN=@U%M^1W-_rMN%idioL z{jDDZlY6d|3M+M5%e9Y6Hddu=4m_rL^Z5-}=A={dRdM4C)B@o=-0XCHOFnP#!*_A! zt1iRmgy%(lIwfPZLgm-V&OfyD500Sko`fW|Ng%fkFvp!R>SiCCI`A%AU_wrrLSPJV-mp`ihxmL| zKKWrsYqeQ-R0uEUzW)sBj?9DY!aUO^I%{TPv@*6PfRKoGra^JY-2N1sZl*-A-94&M zxv5Klul1Nt!T111ADcXI_Zu8^S$-x6*C0*1eb^F7Tfk9AdmDb~B@L$1Je|{{R-gCV z7Z4vTF##fJ?}f8xxCpE>IgAEcpXZ!M`aaBVb~1xAS=6mA4jT4W)+J|fx<4xcwaa&k zZUw>*|2_7gMtawwr~Y_A@~79i#=~J4L55Gn%AH1Pgevmag}7IXd?$K2|H&N?H4S;~ zS^4{2TxlRe%}Gko_@zdlw=DvZuZ!NlN=tHxzvCF*0S_>KythjVnA}3ABc0Fi*6OAl z?wXtuDHZQqlz-z;6GzYTMM}T^R@ti7ZN&ZvcCg1?{)ZFn1Z9*XkVtiQeU#OS9o@Se zBXHp^QLzu`Hg^WD)T}UJ@z}X&js?G9G_t^&oD#1fRX8{#6%(leZ*2hFXO?e!`bbmR z_}_kIE{6p))K)n_XMy0I%h&ds*7ib1Re$L)i*V=kbW1dE*UMK%`+InLiF|jon3c(# zy=>`1pYuT$o&y}>bC6S0!2Dq}bi6KODMmn^5l|yNWCSM{r+Um@&nYSgu29ooHqBG5 zCn%NNrMrCuTM=t8(bLkim!p$J#tjRQ@u7wRxzJ5f(!R(w?S_yT1b#P{!0F~1Jy(A` zfcEx&Ij1jdL?|!!t*DFnG#*Gp#8X6fBY@>S5M^8ln>k?8epz3cm*cwaThMAv4XYS= z5Kna1?PL1F5d($8uCaB{TYYfDLCtq}v*6|K+;SHbPY|T?pGS_JUMdGctYCA^d2lU| z-hC}pv#AzV>wG&)IIFo7&~2^wVGTd2u}6M?JheS+ow}qehom*kzKO2@Squ1#|J!`! zl&7)O%i*cyMFU)mko|x`TeR9=*SCsjwwad(*s!g!4U-bHbGfxr^mpdIPJCY4;ancI zaVkTrM%QOw+d&%o!040IB^+Hg^<1;{6l8adkx)DuiX5kJSc4VrzfxhX!s(3q>SHI? zN(tADe+PZiw zQ~b!GNOhv1;TR`^aUK*P0X$0RYI4{@N8!S+lV^K&C?i0QT2n6);=I|S!9znCGT_$| zdK^CvM6pi+1v0IakwhA#mc^_#=m~6~3~HiUxL=28`sU&OIPL4+03RY!Z`8>)@`rVp zUL#OTBWW>c@LXeb3#V@2dYy4iv;g2bR@N_Mk`>;MMV6gWMsDl0h4Mv>R|h{@aSR=; z+i7@p&Ydil?A-Pj7Z$1)pQmmDz1#q3le|H)cr*}=0i8d}gF_;55*0!Qln*~*wbGe! zezM^78f>H&J**;^Wu!c{L=W8rZ2~h{PQW1ryT+NPJdtn+ApNLZ%3oD-^PWBk3L5!? z=OCEWP{dK?>uq)0rVPNl7uzHVv2A7|?Xm9dO0+Z)t#zwXZ=8J;uV`fF$TMEGb_{T! zSc_)sg{~-GJVcoc6!x~qhDe01qrqGzsAoJ}#?}}!cc$_5Bo#oXal*8hkIoFs!KnLG zFKgf)gr_4tO6_mU8fec9;1nrEfFAC?f9KRp#W1{=1|X2B#o`JHx;Zr92|KgXUSzyL z-f;YJ2f6!ZcVg9a5Do#DGMZH<&iFOW28L(oT5M96*Oi+VKk~{;Md9#5V+R2HoL{xs z0nZT^aQNu>l&ANof=K0F5C=<8-4_$Q?v>2>c8Gz2%NWJ4#$|FI5TdHxm*m& zxY&0>>%O0%(_9?9Eku0(#jsQR=mXKZUX?p)I&Z=&|Lb-#FAvSe#IEt3<4%_ZN*M@; z+wc2y)#weJfl(~8(P5Jc(!IxpCQvcflQ}Dc8FEo|NuiyFoco)@Qc+Ckm5FdOsBX2N z%TIeE$$)euUa9z%hUi>?=kZ*9t4L7Hx~~2gu&lyi$#OvpNQ~1xQRw93*Uui8aarZ& zCIC~0C*nF>h0YXF2P_V+iWc9m(tjx?B$cJG#aux{($t?xyW!J2wSq9rw1sS%j<3!BZiZ*)U!J;(~%4Y<3c#&_i7Nu zZpjV8z(zF+s{?=8 z6BNM3cK@c4nC5tgx$-zXl11gV5Rnbas^yjIYr|iu;o%{{)Us|Fg|n$+U1o;xZ_K`) zPr|5q+1tRgEhFn6p5;jL z!qnx%jA#f|Mk2O@XyLC3*}9Oh>Q#^TXwFKJLQvKXPzZWSCWA3TL-{KG-+UMVvrgp! z7R=SqwCQCp6NYSDWz;tjW~#$={%;1jzTLa1LaAg>ASpl z=cN(m%hvFqri2zxh>)uMK7Dc$wnIa5!JpLw`Cc^mzkcX8pdWXQn1OBg1opGKz-qU4 zQo+VyL``0KT9$9@!tg0xvDNLxOw0JPNl+m3BU}h9DCA=;3jJrf-_Y%EL$a$Vv*(qO zyCaPg2gS(X4kp4&Mh!QH56ym{43DAg5?pMsy%jc};##h>gJ5LUrQq)DabB;Anhqw0 zE;Wq4Y_{+P@2qI{Ww{$jh^D-y*`aX+xqYCU`gS@S27H^ zg^Lz|&0My-UHV4;*ymNl;jb4JNLk4uqlI-1Xz6!az7p{R^@hIu6Z$H@N)aI~d{;s! zDlrt^c}nxJj;K7Zv)iOGP8NqKN4rFaDYNd6hH{Zn%+yH`0^xSgU3XZ7*PaHJ7$=Aq zO%F`Cnv(+v=%@t)-AnDTlF)!+cp0POH4R?->`Tp8=?Tx=B`S3vY<*zLB;Ng>UXHJ+ zJEIPm5UK{j<*`$;b_xAZ(xh1m`LY#O42(~@`-oljp(+X9W%^V zNPxC*=mM}Y^g!>71{}Kq27lR1=>*S7?~b01xv()P`%|DB9ly_eUz41Ed;F3~yq%NG z7ZAV#Z-WsYGwU%Orp=XT=&_BmLjm+NV%h<#&*4Hb9|Zhb z0k)K13cl)^4~K(gMW&iA(;k+G~kCNm9JT^ zJZrKVV8m>3K3NGZTq`gh=RBnxOl&LuieE zi%S$DQghnADArQI)V0uPcS5M}^4ZeRWv;^so?%Pevhw{P8J|tnIsNIketoS8xJFjt zaRV3AsbW062agJW6mjkU!Vb?_FIlzJm6s`aKUM90{X9$4zQQ*nx1kSh<@BpMNdTYB zOrprQDJ;Xw?w*!D!b`@3>_8Lq4iJ$4<@z|2tQ{*7gtJoB{;S1Ix0wC3@Nie&dPB3yRXx*o5|HP?2#*Uc&hyD@1im)AvE)|5yc5oodw(l4 zcR>>JyR%)gaiN-Pq21~lDu@JIt3o~jpG@@yBA@ny5NGGqC+-{0hh-xKkvCTtBX})d z(<>R!ALh)?{P`b8Tx>#Rw#r$ZhSg;8zLoz3ds_L?@ipXU4-(JF_X1I@P8|*i66KDM zA5z7ViXbQoGdy|1@e4OqO}l$ShjPg>wWe1xWhN>}U&LC6Tc|SWm-9KBx4w1~Vo&zc z=PmYSofjYc|7+hd=3k8aPThb1W!ZNRCBRJV}1r!@X z3Bk-~cb%D11yH9bj!HkwJ2<45NNWDk*~fp$zRX;`c5%enPl~*$y5J=U?^!D;XG3YSkIMZ2%v4-QZ4Y{ z+As4=)H+|Kwc}HWMhry!g zwi7tEso5+B)E#gk@ui~1tupr-d6l~E)N2FuFK%)>O->U`x z<)e7Bkg!rV?EF$urPo#g($nNHldy2-OEPg#%N%)=T?wY3fe^|I)V}sMRU#*NvaSCL z587pSdhYeq*(KDz7z2EBF6ZKjw$N0%<`Op}$V4DQDULu1Kn)9b{iZJ}XND!k-14JX z&_N0mG~1Hbzzo*Wg5Ds>*EOZMW5A~*|L-T&COxCcgutb%hp3k5)itiQw}6;aWZ;EV zBd|3+M=JYaSpmP~eC{GtGHsosD?dE$ybnfT8FtEdw0>sVuTVmJKE(kkYf|&?d*PE?1jv|x!;GaMCZYj# zDfA3|e6zG3C&$tR&T1?@G#?cWEZ?OX&~TgNFKY1TqyQEvCySS_YxoC^TxAKMkSB7y z+orLY0o6?-%gNKHfO0O7CM7uLY5K(2;NXQ_cJ{oZ(%W13ti8X*=y|dY)~d4`XlvvA z)UZ`=aVlX+RwrwItvotk#bl?eW4?R3>MvD$DMN!Uy{ETW|0gV>anpu>9d(&z^$PwI z#brRavIhiZ2@Y7o{H_z;WIj?fUAxH{We?jYc~)tf!H=x((f0f1T9(yETs&kpddFPt z%U$P$s(MM6{Za*x&^#CH0AW&?@v$-qnyPJA^ zRaT}F1->}!m&nE#`Wg0$gkPM|n67uM{vM1IZFpI+mNYX~mDj4LeXyX*)Wvg(_t15s zsBR)}M4$UUZBC{ppuigo!ru@x7)oi_VMX2PK^;Zx%(v+o`(#3)CaDveTW6VzKbr39 z9JgMcMs5P)Q3Pr}tToCO^V~SFa7=>E1SDoWI7G~I^ONK?uskR0oJ}Je5lNowYP}%Q zp_FqVufbP7VJi5T;rQ|BNPc*?chbrF-%QBz-8T!(6G63m4dSua-t~04v65boHBCL% zVnXa zJ-?-4+WIF_#cA~+IrlNA22kY34L&RwW8mi_G;!eVP4rV>F7rB`&9IqO~>mb23Ib19NW77|fMi z_>B85?sjSMuMT}qtFHyB+n=7RWBvlFJP-biJ}h%#@W6$pxvc3zJFyOz%kzT3U%EQ5 zLj7h=ofm+}>>e9%tgO3~_+{P7#43Gq#9tS8Uo1I%t@rF}9C302&}(7ON72nH-s^;T zEvZ7`FOyMf6;U%yuAG9YB*wR*>3{=7-MeE;a6P{rd1GUlFNal?oRw{9p#y@1ARRpy zSJ43J zeYt?9j|57vKmqR#JB~+h=D5Zg;Cv-0?~V%}+|n|+(<1rVY9*KsKOioM{kfEhqj6Yr z)p3z7-+RtbR4iW$l-Oc4R1%C7$Ei{%j)3pVmj4)YbHhArUUsFekh8ur!fcW@~m3Go| zCO+CQAG4=ph~Sm;zk?Kfw`u{Zq)mTkoNA`Qn{7?!rvF;)wFh-1_k}i>nBpZ(X8M`^ zHdn;NF}spSj8~4PRx257KV5#utBEiqP5y1Q`G| z9$#a1Sth@Gd2V{@8{V|pzuQlHfX~?oY#4Lto2@Q^8Zo@g>H2MJVxCI}*xrkUAzf$4%Vo(< zz*AOX(5L@r2f22kzP0}}B2A8G0s?`@INyAOY@ajy1Cr`YkEt&#fc)R-!?)b-x5oNv zpF@MoAnK$RZfg|9ndXD-DN`aEIl~`U*&SJ#*0>;=g%X3DgK8A`n?UZznHX}vc44T| z;9d8`5#08hW-ra3JnN|f%LP}7>Na(;h9h1Fj~!{_5(}ZUwufSON%>umI4sZk(BqFw zs|Hk$f;^&6tn53T5W|;pUV*znIt%4Fj^!rxe0gPS8O3y$Xm`ql&h^?V+7gIBlTj+4 z$J;ld!8?sJztzQ(4-c63GIo{HEMwDlnMmtCk=^CkAsLANTz7J<-pSG(KCjImX}m9u zvB8IfK4aTQjtX#QT{tlhOr_N54JXq~Qz2RXiMj^zu48L8?1rd8w*JDWG0RcV?ys64fXFy5+3Zyc6^)H8X30+XZU~O~ zk6aEAf@_J+*^WdAZVy&;?O>+Nh11%qc)Wp#fVcg@k#(${{CZ%GOQXV@LUW0)a3j-7 zP08Dm^bwbly+Ezi<#Qv(oBz|^ng2umz5jpEpb<)AiI7Sdd)c=vk;pz{-z9sLeU}~h5iFvDiFl|08XyL|_?pA!x3 zPZT-9TWeDiEDd+;q*@PV#-|!8{Fs($%qtu4--)&WBhi@ted81*Wh!{E-J$E zKni}!v?@??X>MOmzI2!k4exo>@)&G3&4jF2KpLQwZ}2gGJ)4zaR8-HYDU=aP%x8(O zir=N>jgXy0r$*2m-^#HRlY;MzLQm*_4U|`)m2T*Wr}s1tXpqs$c&J0bV#G3@(oWkV)HDeh?jXz zB<){r*x2)vZ19otbC#i2y~XCYfi>$RL%9?Sro;+p@msJNf8acKHt7g}or0XMdC^2; z(WZ>=aY!l+OJf;R-IZz1rImN$Ak8V}30-?WRJUr!`P`(e&Di?KYoIr+e0DOx@?k!W z%S^;6Ju0v)>Na;2^G0e>oEUsAi%`YHqot^#d*RWUyy+oKTzPXW`f`N?Ko*e}(H$nd$&sc4=|JUM) zMto1+Qpv|8TA7@dL40IQNVRy?1xsPLPlNLj9oSi*5>7~@5O6FlJn%N<)3O>#t+PGH zExPN$b?uK7s~sAu!z)>bNDqh(NW=*Z-)p%hJnNm6<%RcwwHK83lCLR1-K}Nq?PQ-U z?*D{O{esuR zM3ot1s{*nTtD3*4Q;h~37^Vz5dVqobzFHMW|N2mzc-hRp>F5&sz%y=;NbhN78$w~%|4JjuX7QMm#R*@5>e z1N08dsKJiS{ZTH)Z@U8eSj4H>P4;Pa?Cdf=J~SWkDZJ0M;Wh;ja0(UPW#3t{V(Ih% z??9G^$)Bkx2^95PZwSW$Q}SdOToC0(%hhkYjR=uh(lpqC>VS}#}!!~B_T<3tggCbB7pLkID`#)YtJxc{~YCn*W$M-qy`VKQ> zE0)g`K6g;s`Z{=06YdzfWfb;rrNzS=#2&w1xMX#3_@Q)}GQ;U*&xvYnK~RBs4MhEs zWuzkkMPOQ>dgZuk za3`3`R}`S$Lso`5Gi&kRi5b$E-u9bCLY5Y6jv5Eob@9~!^OZa_auXh2oe6wJ5aN*E zR*_c$9!}W{;9fJ_8a+@Zrb&x%Z-{x*PK9uKKN_AXWw6nN_T@p9uOO@0IX@lHV$zjlP79zB7l%FKY^GP-APbS1q1QZ_ z^z}qCtWnGd!w2mw(lWXUyFpTW{UK+yckwb2`}tP;$6t z6!fifebouvinljG4xM07Q8qgxaF?O<`S-}v8xLc+dJJ^wiQ=H?h$MHty-_zq;eW5I z=5vMV7cJ=Fs{^;c()7xt>EDk7HPY7&M9pn!M7O!tXQm4o-Rc?FzttN1&d&UVTX70A zgc7U3v;(wnC$1xEs~z;V+FtBl6JjxoO=GV7Tp+|w6xskQE3mp>L4++xzwO;ii4w0= zi>%G%0z1`<*@{V$9-@+Fu;O~o2ulMYwPBwmNe|>br&yUI<^&vsYcUGWZ3+nx>TdtZ zHPj=QSTwn;pEHe2q+Ro>@Ck8*{ipC<6J$g^aDTW;*yl?C%z9@3ae{Pz z+JZQn@13>hckNBRV9}*Adrulqn|F`6^%}euT#^`PWQ~~eQ$pdOXv^Ov{wXM1!B49- zg<0x14ct0w5!Vs7vZ&~thB6ylSyNH|(5A1 z$#>RsvIHDlvQk3Z|D%DO(07Vx2-F?`DOK!Q*NIg?Wk2%>h?Z;_Gr3w71j-y$cCR{a z?^K9+H&)H8X131wZi~nRB{u<_x?G$omR&lR67Nasm9J9?BZeb8%Fb3bkvjU)qX*Tg zyBeAuH$OIQtC1*2UGf`tjc}yNeLw!9Ax*}jPeVnJY)|pOAHydoQY6qP+ns+JUnBTH zjq!PkxT->Vry*m|g~awp9GP#P<*c*IT*nW5zK7I{Wd?c;8zY@-LRjn)jbr`He?`m= zHHd^v-5X_mrU(+{JHwaH+a{T3Lya3{st0s56sj*H-B`NBxTB&i(#IS3I3%XsqAm$B zrU>W^Xa@V=Q(4Qzotxx;b-Ce7;X(6 zD+7&me$@VSzN!(U@mHXc(Xr%9E9NSTdmwq#B|pOVjDGvNHBK*EKGb2k z{oe~k1f5{|BCkzc$N$Xdn!St-g)hwtpm&kDnZiE0I~!uWpX||goqZTPI_i z4Tak;@zvbz(2IlpCtJW|QcueR1a|8EyC-f8NTMfUl!iVnnFq)Vlrw3Z!0qT z^LNe3c;fXvbjDsdcJ_UE z(%NS9li*_MQ!47aDLgv+hMV5qwSVjD9^Jd5nWA=0EpGsQ(2{ld%6n=-_rZGfF{vXc z2R`Cc|87Ixx|goDcxm7k{p$_1j^($B_KmGxc+C1YR<=3RSX2J?um+d$*3(0vc+sii z=W%lsXg4X#NeTT|0ox@m9~H&Y1sgz~ak49_NJ$vvctuVtK#XKih32e2Tq#G*!0p#P z*=xpuX1R(5C*C^_d=pkN)N%({T=87w?u>?IN!YG_u80=)A+xpNeNzWuAJ|yX-5YK1 z69@-pJE2E&bg5KSg|t+H^a&NE9?E|3#*2t8)zlFYkf1s4fyyAA zR<_%-anl-nyWCLf-c2X}n?9`&*%fpqDWgh>2?`nTbDI-;L$sXT z?M>B^tQQFHu|0*ls@oDWy%+bez}j?&XM$B*FnH=pD9F5)Kf9i+$rmo6xgT&Mkd*QL zQeSpD5I%C$(8fBl0P{J7XGW2H7@kP`l#w8uo?PNp`YwULg~hm$m}UKB`TQ{9uWzQ; zs5ON;4ud7|er+zIwwQDhr*O4D3I*{KcrRl@4UD(;c5ew+1+^&tK*4oXw?ECO|M$=3 z%;S~zX9GM<=5Jr{aq>RxGD)2g>`O+Ykm3Psg7?EUm0dF{(sr znRn~E=_~veWeRJkEslA&XWAY(mg|VBG3o?5g>;|C;KEl z{@TqDwRXr=DVB*ZK*ISmP0FHg?hKf`Ni9J>83Hj?+CN_KR~gYP>UKlAV1| zW2Jj`o7}PZN2$Y)j2ds&`o5&KFrHqn9>%?+UQ-wz7}_DYp4C{P0HUr|LS#o_&nay4 z_i|C>KpaXBug8i04EIseR&Xe1V8W(-Qzo~TDGiv45;!jVqO)umPj*7yzu3>|t8muvX$0^p zkWI!9Zpkd>HWS6**Z9XA2K4Vq%C7(@$d`zX0jGl}mI2($D~~wQaMs^Ut#(~)Bq^`q zf0V&E4c^MBrX(du^DXs)q7Y9OMprW-A?_|ZE_9gIsnXz*LIYE};6yLwbK*Kt&N81e zv8Aiy032(Y(a;;$kccjYmM7E0KfV|AS00@{GHNdyE7bld(@TBVhbW@fXB53P5{Q3P ziqRmC;K1e-P~Hj5e&s8~V?%g?pzdD1+yQpUFhdYxE2=8M!5Ob@CtAJu=KeF;-5ruH zWma9i1Fs%9cl^@RcZ!eB2{UdBE7}FTcLqU<{CoI{b|2b*E+yB)$ePVL}FwRih=2D%`hFT<076%$oYC{d&L%ean9c_@i#Vm}+B}dh# zn|4jZ(C72-NzpCK<0|{Vc*P`E7Ge;$Y-$^60m|__5cP2Hkn+|p|@yyCJl&+M>1;8n`7f`d2x#`M~O4x+kRI| zBzV01`iSetZ;&EfZ20}gtV#uu$L3+JRKg0y=EkzuS)fa2ufoZQNW?qW?m3@TNTp46 zMOsf50M-U{{mRF$vKD@>GT?^;WUATt(9j)i=_wHVD=w~3C_4Y^6z2L@Zy#s)tfc}* zWge>i5qR5TP-v9$Yj3& zg?A@NSt6uvmSW)q5R7TP_{Tt3*gz*o!ZRHq3zYLME=8e{1Xf-_vVEROw2JRCso>Z8 zUPbwrVt(t+Q*?e6YHCHjHi|G^qFtv*)5+De?#q)*Q&~Kl;n~qk$4F1P`RTsrq-x*W z>-r1nIG_dpDq>yT)~Sm<&o`aYu#A_6Zh4hgciJC3IiJ2_?#-Ep$gJ^o0p!WHw_yJT zHawOt@y`n;;i-hU{B8^4|28ukefM%457!2EQdYovtMa8Ex|Z4E3!Xv=JVg&vjiM16 zI*D>IdKTQd1tgExJJI&bJsy|123B8h5b2TKg>Aa_y9hY%8(+Cw%HYKEbS-Q$HH{E4 zOa`-adLXqW(6i*gOan;!4}aDZ^W|ink4S8_R%`5QeKm4Zol9xTLYbC{r^!0i zVTw9so}VkviZqiCX3L9kFC^;csM@~nE=U>Mbn;h+-0ToIcs?rw_;@Wjs zjxCgKTUEM=5o#|Y-o%eY$s0Fxs;|G1Dl7L-3v_Z7U6#~H73vHDF~;(jNnD*(`H{Lu z2cfkk#_y+r?aOr-BkIkY4MYF6!(iaQkVr4(ea zBcQg5lrdvM#j4K*s`{qln$?Fzdnl9pO8;(xjl4IYq zO;+jh7A+j;mZl^9?on|U6;J@3`m4V{=hq2l$sOJd+UMV(KD~Zt?9mPh)?yT z0#tX^SWBYrdq}&^{uAJ<3otL|BgBPDUGxy*$zn+c0RfELe;qG_Q^ydUR~-YvjAs9l|-_A%+>LjYgNGx56^&( zKa_$QH)5t7wledN=4nk%eb4K7pMqj_h1$0XaRtc_N%Fv}VKt%|GU+bxaXPogvauDU zSo^Mc_uqWw11H;t-LXxT{BUtG8{!QN4y?=FgDatvj+)9c)xq@aYXPw>aqDB~JkIKX z{+n~Dp^t#?KY*$6)`|H6?mM!|kED{AL(q(-3A6Ngmk3mwJ$lumC9%#R0bbM50 zI#(=qtwCibYTkL@sY{D8MYeeh<~=)aFQC z6cq9XnH-#}a~~gesmZB2&#+iO(WEyIgCnJdCl}**91myl@$GsL9Z)=3YbZ8^mTpF? z-}zf7=~~bE78!-(PEiKZ!(^aAf9eS!Nu!^G)XOqy^$JDXG18X)mp0k7lu^*c9Sw`H zN*>pQKaaBt2+=a8e5sUgyCaQAv@WvmE7L;_uM_30fsCq3AM_RG%PKQ5xJWjt zjUO$cnpCy8ym*H$gL&;b@&S5HN~Y&ok)%jq-KeZ!;~R18LaE23R0aF8;23*QFQJnS zo5HxF70hPeQ5**`EF`5$%nns2k8cwMuc<30VOXSF=dm(x@i*yFcTJ zDU`lCEWrCL@wD{{Rm21TPEABIDXT~2jQ3=HPT zo)$jh(!`xB8Fjh`u z-xDH#f8nsR4MdAqhmcfbNc_5ryjCa)1}KuNXsGFs%{T&C{Z#XNg*TMHA~)Oxzs>oCh#k+6j# zLxR(CeHe=V>`0cJbh4bECWs8zuGD4lm;i4exA0y1PGpr`(WTS95N{WJc1sav36^PJ z;ZmH)I1Ogyl^VNgIu^PHs@Eh~1{Ac2Rw}w61%q1vHiC}K07V2DtaT>@XhIg|U%w)1 zS!Szon8CusAoqt52K5AzY5}9j+!S4;&cUU@Mwv^7tFVIk~*= zb0v3OahRT~O^b1IxL92nsfYB$M82mXg-u@tQrf41H|z9oJ7K5Zk5Cmbj(ZE2hC&BT zVdA7u^E)0@awBzSAIg&ifbVgth}LrO@mKF?19~vU>ncRjtj*h{zH!L00p)FmVky8x z()~Ps{$+OgbDq04coGn_{74Ny5M=eT?{-2d?;`w-KgE9e>6@&6rI!GgRS~p9!3&jK zby!y@`>t`rxWjkmxi7$*@Ac@kgxa4NBw4xEAShBg%AA!3gH61HWz9bUqj+a@L?ZdL z<{g3N43P}=ZBvcy9VRQQp()QbYYU0z%T>OscSDY{FrmWC zZ~V0ubEMyMBPkKP9z)5qt;@|bUcZ+XOWGD531i5SIPI^%=@ubsPPh8aEYb8XYCEb; zrC`OkvJ8-b=*gzZ-XB8PYlzDg8DwaM$I`Lmxa5t&ylIj7IIx<9!^kbZaVGRxx|eUrRh^l3QUQmFNxx^>7#>f#i2 z=@Uj#wq-?iMpMDSfacTZp|mg_b1?&+C4ZJouADZ73>6=PM779A{1&5?6$@VPIt3xZC!blgF`Ob^r zouB8=u@`8cfHbgWZ~i>758#^$H9nI8h`F}Oi+jKv`o**B>DpfUM~e4F#)|D1nq_|8#%3Zs ztp0wjO}{w2XY~gYM`BhT>?=$80zsY6=HZhFWl*6fO_SK15$aHLjN0=e4Llqd!117v z*VL*^pIArB66hdQFXBLih?a{j(!w#)<`Wn_bcR1dC3<%e=t(0{XTtk1Vh%S-^@qhn zC3QAkyh}-n#AyGBX(Z|7vjyD+jbamKXTdLTaJTJBXy!T4+N7B&#PT(Dr3nRnzr(&@ z&}??KJmb@;MHCEVqkk)G0%YK|ikNuC@^Ihs0uQS~RNIt*^}b12KG`&QeHoGtkjp4y$yx{1@2qO! zl@-|D!fI*;#{)j34<{7_kiMRtnSU^ecs1f1ABrdr?+w0;kFX6YdawAP{7aHPFT@te zUQrIIy;Ib|_U5vE?FZhK^tjtY$*K<@1J8( z?;3fK>nDWwo>4Lnhp2_eE$eu8CH`E;;4q(K=Q7SayB)sPP4_|P^H3~`e0wJ-{~!CQ z7QeZaS}9OE=HGDK!2z4uR!4LhhpEU%0qZ}cH#YvPmY-N`p1(WMf81b!h3e541h_Ng zSCDaY0P%q)h40yR!+GZ^soHs9bt~W)GUERL>fUbg6JhGDmFEF_EVO6=7rm~HwHGZ< z)CnO?0b5#M0DX_V6s{{Gv*{wB3~GooDxHrJWSx}sa0ur^Z3Ti^(@}0JE|T=6K#6SN zbOt^^N+ziHmd>RSg8`d@ZcA^d*<)(*A%jb(_)x{prqd0@Efuo8;DhewH`!f|1C=0;sY3K3Qd$&PaiShF`21@7M-Gl3?EZ zXt$EVo}%z#wQoTrX1JaS%k1`TCQPt@;H9fwuaWDNjJL>XQ`1q$sq9sV5Qq*$=WO+r z0YJ@dgfy=V^MStlWjm98j*YF;6pe~yK?$&L^JSj)9^HIpn9c&@VG|E{)GxLef#^AJ zJm37Xt=m$I2OKd_qL6)bs8%yf)iF=WWQo7q-%MH3$mcjdPL zz6hxfvVZ5X9?pEJOFgEcgF#3qljXCQWJK9bWlNCh=vExHKk%`jNq(%fQ*ZqD##$F? zXU{?hhKhSuHV0197e`#Qox90GBoNl37yJX#{2>*{Wf*Ubp;v1B z*)FpAxR6{;ll|w*k4|KJVi|`2u}pfU4lgb}U~%eUYsT5sd2+3HK?UP(uXq^MGgk^} zN4XME8Y7A&5eRH*61(330o_UjCtXd_qKJ`VHvR$#niyw_6ke1OQT8S{t?P`|k?0>5 zKz+BCIDb-NP03@ty0-iSE<0i)Cw&p}mbgYflVV|~ER{KZvZc8CGUEh0;=&7Xw_nUU zt#UI-Y^u>|pH6j#)~y^@b(u!oWphD*^npD*Ej>Eo7tlR{ zcWzq1fNX9YpM5o}0ikDDkdx5`!Up_oZhfDz*tCwn{?V~iauuhz2~su42Yp9gFWakPZxbufc=$wt_UhjC0Rubv>VoxW=hHK%l8b4HT0T@fS9w3jP(Yf$ z0L53@@|TVYGnGl%kq=ab&nB%7)$IPk{}G;erEOBzJgkogg_*F$YK~qO*)Y5&iRE*k z!hP(;<|$p2E!iG+isQF{l=%Q{X+Pv- zwD=mSVAVf-VZ%7LhCCr5!62Sz5HwXJVazl+sUVvR7e5iQQ`yJ(;xH9_M``AZ&n0w3c31G3%lTc%$! zV=*igY~Uqn*s-a^Sm-( zC=B-juqts;Zc||_;Hr$`>7HEbeYscAtxf`1A&sZx@^8UfmL|I^Zg(yR0e8wI^(Lk; zh!nVNarTW%0uyw`kNf4QNQO`I?AZ?%(i}zm3xnNWM<(kP|)h$&D#jniz4`NEcz}77t z`mc9N-?~qI|8&o-%EcUtj!$9-f`EcuG{EDj)Q(?+SSxA2REHtzLZ$#*bBp_+>}FlE zZ4hUWvb+w}KyUB_JMhpetefL_SWL&?UxBB!KXM`RX@E_UB0wp3KC3ob_ta)f&5H9Yq5WRL zCBDK9a|Lw!#mVmmP)?Be6MhXp0$fd6;zg{J)vJv6KV5??g2?U)DA3T!NIzfp_@kA< zMq@x%KY{b?CwA``78=SBcuNZ^EftL40NHy61ddc#kRbd8q=p7w(4S$_Ezt54B-Gc`KJp%g{n~#U+PAMD616uuLy<%x_~a<{}To5*X$G zw7uZcXG}Yxpyj&voerJ5oh=Yf(r*VMJJ+nhP&Fd){3Gi;Hh`MLsh z+YgXrOGX$Bq(DJu>8uDJ-d13%<@9m4^lQ!UUV9y({>ymW9pf_XgM^yn4^6S?c)<0S z@=aG_{m3`5s)c z+}Y&i3XKYyDy(Q4_U@KNMk#(9jsAt^9Xp(HO4^MY{%6feq~2cE-IKdC#f?N-y!@kx zMTBVEzuo2>-q^Z4=j;~5(x&T|weymy@5WQvKtH1BNxX7qZz5mjP_ms~!aYoYH!y(j zi`c*F2CK0zPOrp9k(Wuga`P{j;$fW+4e5Ll9OAaR@E&qo(N4Fmrr@D>+PPHy$@MOr z?KEfy<)zH1i_L)$nUlAbos?}pp7nheat&Dhfa@`Z?AJ(#4leKlEyH`k2M{9UP~n9& zE3!9<^a+8!*d1z}?Wp$Hod?GAQHqwx&x_45Y>fxb zb0QYUzv=Uq=Y^8N z&3}DLT7mtv_c}HmlH?H#3J3ug{{tKbNL@-^AbI3;r5NOyzQAC}@_A#kxouXc17P~e zAU8C3pGr#)ex8u#p^fN=hjG~V*T@z=&vv#keQ-R`q8fIAlnQ+vdCuwtTx z*1Rxsss@{(+P?N2PD#lFiKPjJsi_C90_kgN?VB9=U0xWUvE7em*_GuXY|H`fZ(-|e zI7F2aAeV;k%UvfDEOG>#BVV4}EE5sfZusZo_TcI_sV#N@P)EmST-Jr$;817xJ&5^> zfNQLGQbc^F|K`=bP!@KeLb;6-Q5EBt${i$?jDwYn)!1^uU-LQ(od3r_YB@`tdi$eYV zGZjpqC|I!hj)+nM0Q37$h&;$ZHuKl9_ye?Wg~g<;m))mfAc9+XnG5i{L<%d@XTYr{ zWR7Ub79t!)PY%z5y;nu4AgVyJmaMoaVEP@w7ZWs>6^lSc?-0H^iQ5GaZ=d$6CqSce zah=(}mM9PTOjsQsxpSXy%r$kN`$IxJ{&(s9nU|f#Df6q5mt9lv6_HXao|VSTa;aHy zd4C7s@`u+vv$Kw-+|LP>+?oyac*MrSygR7Cg08o-v}R!tq~)XfuP(P9<=p zSFK$75Nz(~MwMkFb5hASS`M?v-|mB2G=nb9;0hEn5BfzeWDlPmmiMtfgC<^0x^JBa ztsGcY<_Ipaqj$(oBA*8vgair>A$k*pEC`>}Khl{Y)0u&FBnl_sY;FAMD<|*HqJfn} z%YJ`aG%YcfS(vd!9oP}PUDAod1lndlnzSG3>PT%!Cg43aD>_t{ZqOw))ZI-G5c9;| zZn5=(-H{qmVcY~K-@IZm`|_7;4_%(jm0y6wgE?n-2^}Boetz{k+hrqaUluK|tVwY% zvGbDBir!E5yon4V9zDCh*_ZzS5!z9tIRrunC|o#&VoU#u71%-^r63M&PoDK>_3m{k zFlcQ*hA}bhQ6_*R8972Asvs7`GLWw`mQwQho{WA9{h@ug^_U+VjP0U4ikvSJGCP*Q zrr|(A2FU?U;E*hYgcWI^`wCFGHQa}Z-PviazF010@gRm80*Uzizs*fTa5}t$AYr|L zkEE{zj9ccq5f>U(>f)Kb1W;gl(*U~dbR%3++>aK21<}c!TZ0yHv2=hSPG(%}2iV19 zr5l|lfr)~jr7gENoIZ*Fdhf~UA8;SQb1#X+?JSA2!KyKfiGR;SoZ?7(n`r?8Gbz*X9C{^Fl!6t;39FnK$O z^>nOf0QBR!2|j=Wt^mbw1tJt2u>#Vwk1X6YxG{cLq4w}!^q7iDUfJ{(7 zReWk~H0jWblY~HW0G~@j??Tw##b&Rsg_imbvG+ z$zT>ph$99=`atF7eBTkRq0?G&&>b6#aikciae)rB#LvB{|FLyvwu9mepkXPEQCrks z$4dY@mEjVL^i|^EK^Hwz&FHl5rXg6K-ncXiJGZULXfhZnuyBN@dCE;Fx9r1O_dshz z@WNk%W@W@~XQM-mv8m{?YDqJz7Kq6u6sj-$VDiu)>nP4s-B!#u?n~?iy=egjOI*#x$1}gH@431+--6sgi4%KJ=2S%Ez$)wU6cy4@ z;TYAiZiS<4r_;>=2kd}*AM{lP?_5~vZ3x~Hx(Du`qoYG2WQVK3Di;OXM)|twaL(jv z1<|dy*9|}H6))qKXFKhFUb_pr10(x2LSZ3L2h+NnRpM1o>62>kE0`7ah6NLm%qf!& zLQ^&J+TT36u+|Z-o-j#KKe7ArFGk1o?|}dZba%}3^T;Ab)lEw!$^_-sIp&XoiA3M% z6VTE!(+_kJ+AVe4UTwbUVmji`tZw9t5fnqnX}s5Au=hEqYCL|aeUr=Tbl z?qYM~T`gmaN=A*9M)^kb8VU7Z0)U9Uv=5gckj`*$!wn=o&ld_8Q_#pjN*ySiAP|ZF z>jM0Lt&ow#nec!8p#T3~g1h&DC-`B5-!HrPCXM(rIacC7nqyMW?+bQoTYe6t1jh&n Ow54OHA%>DhB!&_MrDXs?KqQ9&WM~;uLP|;+Q6!X9Bou_9hVBMIK%_gQ zM7lxJ_c8qMy;;k}V*KLXbN4-GpMB2dm9~~D1&A4hgM&lyP)%7E2ZsQIgM;TzLIixn z9=)~={2+BvGjhklA)~$ehl`W?krDV3*Iic?hEvqXvI_h{V5|5@5eKIvhV0ye5C^Br z^`Ww&zBlg144I$)VcOkG5r&6q<_~CS)YL>3w`lL*2j`kF6b8c!i|~ZyPYXb%|4f`9 z2dkc5WNLMFn*u24OebVmzP5t!dBq5P=Yf+D2Omq3!|z8Y-sy}LSESa-3%yGWsHW+p;G zK>?$?IaB9Vg`^_=iX_uEF|nF zMl#i<^bKs3GY-#xiX`z{oss%HJ6U5b{k5_UqQ3h0gySYY1nb*aTdN1IqlC)sDd@Me zZL&uf8gwJiaas&KkM5I(wQZgrudS~aXjeVyF2o}g$Y3DnHj z6+b-aT7fBFeLh*dv;h$q8jV^uG_)AFBdQq~w)GlZc%^B}NhsN5%V&Tuy(xZGyc`RK zPh7m)R2K|K%<>7#SVkCjq?ZOKL;{~KD{_ox>^qq*N{l5T*>Ui>UpT`0X-`CsrNBwFnN7yul6DZcL9urBXEf&z@6IoO?iGITGsM-}`aSDYYP*O^ z#ZbdEr%Wdbq^*=xh;shHcK)S3jmZITAHU z-vi7rg802*0?4EHBqTzuRwE(uhs2#bU1uh<6(SW2=3ZW+_1FRHMk`HAM6Cp~Y9h*g z`D<*Sy&`Yyr$mFb=f%95MbE}u%OS0T1V$v4M;mkekkLb~b+=-Vdx{KclongnyR&0Y&7PlcT;SIINQr-y1F%h6IX_Ld(`D6CzY$7KtQ`&+&F$h76NN(}$RWka+8#7e zEtG`ELWQSG_KXZXHcZrECTH%Uwt2=WW}dqp>ihag%_xPbtY=(zT>hjkf9WXj#q0f| zE6_#uw%};Ib6pgEAKI?G8}*Rv=q(H((E=(Gt_X%wB)~V9^`90#*-w^J>iOyBMqJZK zyONS2u=|-xA@=lC`ZlgHDDYrrS8Ts~Kg>8Hu_fZudF=l_xD`i(tT?|oBdh)ih|KF1 zJ7;yO)P)u|x^NOy7}iu0EcoO!&TzB*QOdCTpJb0^j=JpF5yaOv;gI9=Ga_Ge8SIiU zMc^jVBMr?IA3cipJD5LP;0+)zN>wtT7bra+Mdr>+;NF7Ya%(ao2OZO-wzpacJi?`apODx++u+-7og5R!xlK}j9@zr9ntI%`+s%8a;ff<~(cS73HSTrE zL?dvoA4tJf{@G2$SI8tNu-fr$+z6sAJSO)2d;Ni>#a2^YBvLp>>E&{$QBK>Ne8XAc zM1$fD3m2?Gmdwa8^>m;xp7^`!<)Tgq33QRRYgji_TAVh&q_mV+z0w0^xhGqYX#QZd zAipmuWr&z*44K??7Fz&dfzp=uYEB>mU{2D3JK%?p$A-E?SV>A~Gf73)*Vwnc5pKlf zwDvcK&?V)B^^~E8Bqf?@5hKiB1swJWFNVX7ax0B%ep?ysuh76vuxm?5hA&ur^XAPi zU~4Lqy(6kNlioG1**rbl1PR)+haw&~?qoPl)@o^rS}fdLoo%Q?(H0g}M%12RH+l7e z8I%<%7)Em+6_Y~)i+SNZQS(YFVYHS) zfkq7JZiqA-LVec4h*iY(&7v!iQ09D0#ux8&Iuq6YdztWDL#kKgTio-rD~J}j$eAsR z#X9QYVVo75!$UJlL2l67ju1|g3pU<_%Cuszqm49f!|5Tjvs)GO?$REB`5r=A^E zf7L@7=r-s0$r69WrGAc3$$#@k^ZR$!o5m@XC%D+b#RX|SheXLdDh$bO9ArUSAkN~? z48`i*Nc*6ziXUwY!IUFot22jG^Tsb;)Pk>X@ntmcU<$8_Xtfx~ayfP$0A+T^6u9u1 zx>hd`lavH}k!=?c96pl`+b5=~@Op%WFc+7XXmeEA9hDKo-1?{o`6}GuhPw#?dwvcx z6VyNTHqO6J03PP1l$6nx?E&i_0q-z+k@^YFUD)=OI!m-ZO5x*nN0gG_X4~LxRDiWL zos!@cj<~oOfCqDzGuln~fJY3I1G5J#!wo+v%FHXw?qX2ZFi0Y;rRyzv?8tQ-Z#PGM zzN7Mbvb6LfYhZJ1?q|>@+iUVhZRavJ;^=Zi3tX6BZ@XnA7U#-zGUS*Az5?qmcMp$> z;o;#($+Da|enT_2ve@II65xiIFj4~qWhi1|0zd3}+|n_)@Q)~ac0nCPhnVZIzDzTh zoHj*-y`WQfw|WPi5&UHb_%-&@+P_u>jQeNgd`m9=qI3-m z_yMkC2?_gY;Gb4bg&3y+M$71hC)cLW^0a&jATen5_S65jG76LSnfNJvN=WfGbY|M*;( z0EQrPDySvHjtR@@>?eK&VMn4SPJ|HFP*){jg) zhn-)UM=kdMjbzl-%qz*__%?;;KXo!EPBqml)DwnntNR$_`EOJN5IynAXeB=u|1J$U zm$I0T?~-y_FH-E}0v8U$ny@pok4q7___Rzva~G#Obm54idni^Yb@Z zNhIzcj%3poLxzS~SdNx@e$@ITX-M|0X z+cRm3j}q9E0n+xIwtFBy!_rodm;>f<#*1i7qjczkDX#{D=UuYgTP9m zOMsyK@WIntQ=n_43C_GY1PRf&sTjXdAoZP|45iN>yx_@d@v;?xH|=vwUIy&7H7DTbe_cg?RK}(;jV$)RJXwkVl;~_IdrGmTr>>5kiSk?OqLvDno;GE; zeuH#K9k`Z{WiLSZv4KH0>g9ytB=ohmtH;VhzHG4oj4yO5XzDW)+s`{ZLDnOg0=wdl zM7N#K!=?*ku>iq)4NMY7rae|P`!tm2Nc7K(-AQ0W7khqtr{|HKnVQQ_V285lEeq!Q zF@j*;YwJ5(y_eTR6hzJhEa?diaMO8Wl^-|+x!#;>jB7ftdeTXEBHYz%q(GWCl|iw zcZ~PjGLyL{uUj3ybE$+K2Au}5*|IEfjXq~^5r#BYY|5gHy+`Prt13NJWxp{K9XRCO zJD>>`)VGXg6Ni685Er@sxh6^{K)6b}dGkdRYFIlcJF}sHzqejYqTVfoM#E(4bGW2V zZ%dX)CTKXXJ4*S%?cQquf4<%9_RuZ93W6B<7Zd zg*FP%QKo~+v{S-Zb*0YJu&b!qiqk{LJV`8}{~6`L?sqUjxJF!A1=;HS93}95=bnRi zL5M7IexY_sLR_33RbaZPYrJXN$lNJYP~w=(R~oEWuL#eFFg#!XePK5OjM%37ZvDY% z@$iqgYl0ihdM_S%KWv-6`;G$x^Ps0B_H=Lek=1o?Z+t7tGU9Z- zdPvKc%XkzrV7C0@#su{?^Jn)f2GJ5Ruh%X~FR-hU$QGmna!;F>@>p+z}B- zbZK(==iGD|uxtDl7@6zw-V^uOI+>5^LTV2mIUOEzwoA_nc1Z`ES6{5(>tFC0mqzfb zV#m1I0ho-GlyrfI;;q6-jNSeDGH;G*@Oxn}GE5 zOSN~pa{lKd4}});_SsC#h025DTboN1oi_@Oe{+8|zJuM8x&GDN?uuS9w&r%I<$=<( zU+U?Vl#&r=KF1p!3l&dd@C9=f;6HIRYC=PmeGQd)yU=_h4DaSOULPC;9EqM+${xEk zxu*9NwmrjTyPAHSNi|s}S$$FSVE=>cOn*_55kpGy=VSf1@;Ivtq}-Id$hBg_@W&p7 zU<7!*V&B|QPz2axrPRF+|Bh|;yO*%P@PFj;i zHp{ftH~jX1n$+9nO17~G7qk0P;0jl;sz36u!+L{h!$Eb_9>`IgwqnZ;y$*)PfH@ON+VyhVx0 z@*1GQxbQc2xopi~2NApEz{9>#4;tkY5T0Q16p0kwlw+n#^^lW~v*MI_Vvl%!Uk{V@ zw)J(){0cY|C1jPhk7*FTq67PL6bYt2;9O#(^u5SZ@z>?oY}X8|TZsz+OyF}7?PJP1^;H0` z^32>Fx!u?W5{yc4;S*%`_K?wiPvDKkH?wGSR`UJ}qWCe~w`0Kp|Gq20=3~7nWbnQ@ z92vNe$Hh5?Vy}y4G>_Xhr=;^yRU(A9F?5FYMp*ntu5Y?RlE(tChe=P4iS!E>3gzll zcn01hE{4X^&^AyE(A1O(VW$y|3poG0<->1`GqoPHBCb|oEK~rZ5*evhn6b^5lA947 zf3^N<_(X^$8O1rm7^MT1+O#$_L;-~|hoVgVG1vIwBE}btk}8hKC6;RjonAz2nKn3` zWsT?N? z7jh7MeW_hD4)j~Fd@&)Aia-HjVpq2?80U3J@kJ)ORJWc!8vZ)zPU!zxZ7;=nrrxaD zR}P=Qa}lT0!LFb=ev2IQ#?Z`c#m9TtTV_q}%rdLCHr2fu3u!ID*NHRJZy^nTW#IAW zx@Ulq0M0y{7jfRrCn(S{Y|{#LL%Tc3AQ0kE>wvMo`}u*2+`NoEg89NP@h zp5I-ZE4)Em{eg1C5VAZQ`atNUyx`7Og7#z|Hphq?14!%m?&55fueaPkRi&2_05K-z zqByPtpt-9HgJs1|fcExPXhRKcYKqQy7)`HkwAJYM1^Y1PAn z`JU~BEl3obs7=67k_2~if=6 z16zPbMvc+X0`KGcF`w|cvhu+Z{U&wQIOoLCrU3N%*7|RNQ|^?b#Lz%`aR$NG8PTGA zb<~!}EwAYaJbmfPV{Z9Q!!$OB5U%A0{@%;ExqL}!!upm{E4Z9YSXB5!@C6x$z3XwV z!ai=X1tooJe!fVJQLs6X7?7d0Th|iWGkd)B`&|n z2yiN~O z)7s)enpCZNWvRSS&zVvG!%I@^(p28B9R#6FYw&-~%=x8tgJ;99%4g_-FW<>*mYgSq z`ac%GN50BIX$&p*D?+&T?8zd(xU4#0DGnuDi`o95W=6j@Z& zzwIvTTiJ{wDoeuRzsuT@^ZCQ_$+HvQ21vP{UA|}Q zbT0c$1#FP6VzxrH@1@1wg$x_LlOzzt657|G#GW-!fl2P|XFi7V!qYZCtt_wMr+n** z14|RiP*>W>cvVR`g{CGmTXNR7ZhqSwBjC&qKWd2$Ua3n0YeN{e%T|n>pyLARi3wum z*tu}c0oyRX4dj^t;%7o#d;r*&Bx6A1!DWIcpEX`Ng*|!keWf!F0*{w6=t4jcoEU-g z0BQBLY7==LiJPeLd_y_c>Q-zb?%WSfa#&>Q;P*uN>UmE z!*NCM2pqD!Onxn5c8%2@u0f1%(z%=Y?S_5%n(fQR!7BGc-`>H#-9fJJAtpca4ztXH zgQpBx5?^ z2v}(}y#GONcH!D`RX}zrsK<4+-C>iyA#XIBh*tGcLMQ#XqHw8j1?(^(k@G^4q&OA+ zLnPS%tZ^9Afs4FU@>ZCi>eLnrQpp{2$Mhq!Ih$c*oW`WKMvBp-{qRV9rEcn{%O*)B!ZZekCZB@@=J0VrCfRj3%$OB}MgwG`bjLTc!f zsE_2y;HM2`9%(a{kw-O0y@##-eXODz!|ArhsOA1UXQ283{|;S$|1`R-*&j^TDy;Gk z(PlR$A`#z)<_ClbK03HwSlC(`uszH)hwt>_*}~~zX?yhOvOzsDo>gm?u=Rk9yi-uI zP%)InDjQ(Wrq^Pyva%UKzilEC^mS2o+ywd-7Kj(^{h%*7U`<8kVB8VgRCMuqzkDP* zpLMEA14t3CS5Bv=7F2`I$$8Mha-*Eu9<6^8oh86#U-uR1tzF)Hi*$AM0?=(e4hJIG z|Eo}NNs|#FT(toi2&v>92G$_!jfe5jr{EWcEE~M=Y*#WV$OGGNPnXPmdeo`7iS=sA zww*Xo%B&IfNs5Q_?e(2|cB%e^fIQWqe5EpvB!ruPxZdh?fHw52KT}|UgN`yE zZQOGIST)_0RbR?I+LiN?09-3DJM)eK??m=?mB~3`+L0ijt8j(yxZF5?A~X5O?yH z!OIUmFZ4Xb>eJNJ9sp&%Cxy@vX6C40OONB9JYRa{D@CQjB+JH}zgSA1>K_JI?5&G> z;Gu7RjgXWmtjxyUi@_6gGC(9pgh^>s-`wV`AN=c`aVp^0wG~2A3~&mm?Q3iugehSJj9cE3FC=1N!Ix4XucsTPWy@9ZU=+s*}HI1e35D0WR>co<0sQ} z#|n{sA~QZZGeCUFaYof~i@U$9HNEq}>OU{JL+t*btE;j~XK1$}ppH-<=XX>g@8Q5!UsKh&w-i!PLfD)|~LCH`kn(Kd7 z=>)&$uZhJ*WEbn^R_E#vuLh0pQDS?0tpCA=&kL~opZ zTUHUSacOBO(N*a|8LCe-g7}$zwX66%U9{3mTl4SUz0kVT)6K?b%vmqc^>f8JBoR50 zH^@D#_agM27V=1TGo78ks8l4zV6403kIuR+Yh5XJjpLZGqFC062-laj^xH5oG1ab` z(L?g}7nVO`FV*>ZxTH_GqxGp}RV}8u_lB7nfjM*X-4O3|0&M9fdlpGSAioWblu!V;-~jTN}>4+xGnI3 z&TTcFIFF496tRh7TztF$P*ed6WsQs+k+W}tkwT3Qgh~Zk_fcUxAkIY3#c!s$mScU1 zet%oebWKdS14aY8wI4>4$G+ijE3i_F2tgmgFb0(YRURAVBGmT(Iq$STr5hztw7Z~! zKBCQV72nyoO~T@dB$DcL$JX|TXitMFlXBJW?t=*C?{dCVo1uEVNa(QqRmH)`qm)Ol zZet_Jev0^!PhmL&9y-YIZDQ|trR92PXS)za`i8!BLg5^b@0cIoSwRRE4tOidNsy)j z1x^rW{p3?ZBa*VpXTFclNnY@ z#CXWA!3+!#jG|5MU!@8^I`Yfbows_~Oq7wmK`}i?eV|^+EzFCfQt&f7 z$5Aw}-qQbmc9gxQls}pw<}lVxQ91=GyLT6>xOa{9l3-FN_h$D?w+_=!Zgma9!w6-( z9Fx!jW%MC{vTw(Jz%zs{Vj0gA2jmFUVp8U&R)s;h$0~1KA%{ax{G50fcH8gq?&AqY zm;vIMnFW2~t(*!PY3;Bu@awV?Wl1Mf-m7Ypj)CFJ(Ut+LZ)mjo^JI+PO3Gh}YWuf} zSgkQmB;|&=)H&EXO~E_z%hzMtN177}7#V&r$kKEVn`1I!{T5t$e(26%SmDBuF zkzSw(Y@EXKL=n(#$#2NR?!dfljAfkKL51Ip<`eSTCafp`S6V8%ajoOMSt`cqv9L7B z92F20oW4y(=3b&1!LhEp9tjjzG;Lqxo9lURS&b=HQhFkbH}p?VxvPkZOfiMyaIDZ_ zEf`3IFt?PT_e8c)I!-3bQswjXVY~>8ZW-8E7%Fb?MdFWLz}BasL&n5L9wU+k->@qo z79WT&Nc`05Vm_PwH7RC)65(*W*p;}lbWnGq&MW3G1x)a(fQD6aSO#b_Xxq_r<+XkO z@AB|UbM5mSiy19Ir2Inl3Y?@a7GZ1<>jX$6Jg{feeio-%$n?L0g@yods=MB z3MMX$z+}@9Wq$vyKb`&EnI0&a@;wg}OJOdQE8vO8yAh@x5j=$U}#+L&o5DLF{WE_z`iEr}R2UW*k z7y@{tgr4+M-%4^%n%4ZWXTF+%1G-Mahe28|m;bi)C?p;}Q{YL>%mx)2Fk2+cgx7wk zgqjp^w5zX8OgZ0ek8?DAEPvvz0e&DAMAs_7@|Q^wQbeqyt6QqP_aKkLj*QIfp^8rp zD%{8L9sN&mRJ7H==hEGXe4Ar5#dYI>0HH{8@LjWiH6G`hciM^onF;}*z#ulAeH{E+ zT5JDph_<+08;^{Q zX}ox$FhG(Z&Y#B*T+9CP0{ zG7N`IQD=L$0jL`yhS_d~8jfdV?3st5I(7u)8Lw5gRl0E;vhoOo>RXn?fQwU&u_99y z?oh1*B zI@q64SNn>SUcyG*J8ZONVZlFZz-Opav|iVIFy^V6{Y{N)jGB;RQmaoAJHI?m$IYT3 zs!ifEwZ01!6gdEeNDNY+5RV3h<^SeYs?>(d$ z#EtoW_f!457}BdC6c@p0F7Yl-N>{kyye=z_v@rDLj^+3cb$ptfHs8G+?{o$4TBIGc>m@bn*ORzGjxiQpcBuMZy?ZxO62)^~-T#rbq_p5ZH?M7MY!fGj zS=Kx9!u!KjX*r=VB(5gO4OMLk{Fo^Fj(q~(C8K!-p3;m&UmF^YE}tTO6c?AmZzW3g z%Du$zJJaZ=#M#H1LEZT%eKQ$hbkQxLPn=(-#sZI;j5x6h`VuY?qOe)EpebcH0~H#y z1=J!fqkzsZ-)fl^%M&uT$K(#3iw;r7>mHvyln8noxGqG8FV?DAdQ0vYdQtyu=j982{PB4;gP- zb!%7BV98Af6MjKi^+D-2dO88TkGay5hRn4Ft9Kbnv-`ClKKw5B;(#zdE{;EE!bJaz z+ZH4^zy8@{h!~kMX$_PUJB;(^bw{=A!tq+U`LDa@+aO0aNy52(YG8%c0IgvEjoCxq zftQ@=(Jj?Do=h2JRPvfTQqfWHg?d@iUB?JSBbBJ==&+ikQlOFd} zJ~W!3%#&eGP>N7DK5xiHba$~>m}OB=jkyy)qIoB```2-urJkGFBGQwOg`U`8TE5krZ49DI6A7r~LU+FPvH74DEO!oF~8+*Bxwmwud;>k$UoEVqsgQ zLl+WFp*Ic4u$IHLwlLC@|Uv74srZ z?}fJI=T-rh<9}*BVKF-V%x|pG8T7*d0hDcy-n!hX^pXNga$@sqjtvqXfxBp8=~>$ggn z5g|y5E^QD0z4E;uVlSu{EC{^lWj`m-oue56xJ~*g&?41O&J8*{`??H#h#}#L$)=R6 zaf+^gm_WW4ebHOr`{LH4)>grSaEmKsQP*rKS3z?EpAF~#9YCJ9$=}zDBnS96eIQgpq=KRs>VD@Xd zyZIj$ICn@U?=frq=O9HdCt&=ml7j^% zkvM8f=9^BAxs!GmyN?vE(?`A#$%fLwO|j?WV+NIUl72`LX7p$u0Vh2y>Z*>+Kwc_M zC0^v_M)W;npLoi>=$z(B5a~SRP2Ki~c;@f)PLvFW-R#bO<7~r45Ohhuak9s%T7pnm zC)!~X4#U1EszLbT%r*_2t-bmDtST_OC>yhd(q}te^+{$q?P>2W8$3`+JRsBaHN~` z-@>GDG&V5edctf76Y+5e-Z~ZcM0PC?mVNznB0KZ*VJx?*z!&d1;;flWw7{H!rIe_n zEDz5J=+EHhStq(+h=+$qxcrm*CCNY=Q)idYkO=sZCRy<> zl+SPxoy@aS#)l-lujq3@`QxRkXfi4iEOTs z3cUGv)Cv@v233~4+jj4V8{K1Ay*TahSy}G#k&&?K%^|vx!FTH)H08Ksj9zbwUlCTCi z^E$p7vSciLs}3koe+>XWaPaV9-0*w-xk{4C_VOKdtFvEZN&oWomy;sD1Ca&2vJH9k ziAgg3zq(dQc3l`I?tD~@I8=r;e zvJF+r%nwB6>O1mZw@F8_THy)u312oBwGWC7&wP=D5yJ!tH~`X<$RYJrmFi6XGoFx) zZ^n%M9|HN#81||Vp~4;SG+$@-)iDJnBJ!^Ipq1tEvO<@@ou9!(brf4?*2T$BTNTW{ zKK=7|E8&Lqep3TqK+p^+wYi;C6Uxr|#&rmDWU4y7NFSL?+VoT3d6ctQ^7++gvV!;PU);Q@R=k9y z0fpfi7+imyJlxKPdt7!MUK(ZqZCI{IK5TY zGhT`1kW&lI2L3sIK~_=WZY?6wlG-CpTzP zWDHcG^UZcZtJw99&^Ek1{Gs^YlRI5<|F~`&si@Rk=?$K-Dqkp!rO5WGm|2mE-D<4v zXS2r^>*nr(jVpEoIFM_SODFS<2$B0jU~=&ycKXemIDy};9uELnla9{OCQ=2R;a|p~ zFc%PzT2PWEXi^_;pTVI|U{{mQ%;t`rjKXcn=HSC;|8(#Cc_(&XGonU|-Txp+-{AC_ zBxTCi`t$o%xyH`|a#OQzcJU!Da|k+k9pveXIjc2aVL;fVoe%zo5_44nB5s~md|E{< zE$V<;>DxTgdZvtQ!Fcx8OAnd*y|dOl3V12w$uLYnfTzF%z(^;b zW1rp0nw3+BxZwFMAnUi);Os~c-|ikxBQp7%nXvKBFqjtE0w_=u6@zE=`|nAnNU%Dl zWyc>JvMa%(J`v!%=R`_n1FZ&nR!8TYVoE?)pxm|q5Y3ua6B4+P`}=(%g%e*h77C8< zZ+j_Sx9*ZH;tNtZg{~ak4kUtWY}6pw8-FIk+#tq8RVUW71{(F0xBKN02HISTdMRcb zJ~uc`{(EodUmUV1`2f}W;jb}G4E!j5Wd{Zm5P#zNq}VtekbncEngt%_hCHEy;RyTQ$m~!rGSVs;8UNo=V>ovNgL2w1vFm=1HK@ zSB+|D+Rkmo*6gzS_ElaQz$;Db6UW7GD(dLyI1_`7;E@TRW*zqYskB0x0T3)<&%7@GRX{R6pY)U#P{ z@lZezK+XBusQ6~R&3H{!+je301wtmX%UWRvklgateajx-ZK6t>{`I4hwhm&RKa^0C z5-fMf-S^W8BJ}Xk+0{*#$G_Rt&kWZIcrwi`JkBL{`asO?VoqcQ-yG%=7)Z5S9yT*>aXpeY~03qia-makVX7DOA zug4YcMt~jK!YNAqPH1(XJmCTYzU|^`94O#1y`$?mD$Ry0wsX5d5pA$q7sHl5Dfp%n zAw5SsXpJT3eZ3(#*sCTnWy0j86i&CL8r zD?)gulp-7uD-ID))oUS7rFa4kA0-OL60qV`@yDgX&(?+35PUt6IOw3em;C(kK;c)7BQMdh2jyBInB zPrd8Z_@Pt6OO&BTx!rjBHrd%<#e3IoR+4;UFz%|KCKZv07m|ZL6hOBukS5q7sY;4_ zMd-P>nw0)LpYz|p3Ex@ykscT$_@w$OOGy<29xE3Z?;VXly@+q|nzmZB*FOEURyq%; zCHHsZ8xZn*B0`7)83_NCJ0{&K8xz={z*7?Py*x2Es%O6|d(f|IRJvrbYb=(it}hy+EM=(!vnCD7H2 zmJ`qS^)h?@%@c$!=%=x^6Isd#B&^*ZhT7z_KpyyQxOHVn!X;Wbw5n zMSO;opE^L;>@pNkQKa4=SFMf`uKG{l-SjXKi@!2&ANQzlrt74I-6u;@BrQ)Rf>(_4 z)P~NI4ZA z;J3TBAH043jMt^A^)?rmSW*ct9B?+)AA@o73mk8Fap?DvmVX2Z-A@ib{S*GFHKAr}XroZkw+AaCyPCsgwqR9p&TJRV)l&)W;D4x8Tr z&e9!usL7r z27@ebfaF%=vnV2if<`W`bQ#x$(0p#Pd(D96n<_2_0F)&!>lAdf<)hlAFgY0&VNO=- zu|Ar^WO8_T_^3!c8N^(^<-=;OXuV}q>bv|P?_kw2Cume^RNJ30z2HO}Sb(B0)=kws z3QhN~z_}(sl@L!7m5+twSRncDj1HGN%~$$O%AkeRze zWj`Y$r^^Exd}Un40hgEc8=!K+7vH=LHe^YX13mtLr<5cA^okO%V?0nVXGpl+MIcQ~ zfD!|dp~4f`y7wnMh3SQW{_qg6k8+#dCk5%8K_cI_BAfw9$2~% z`bkEuW3r(~XK$WycK1{5(0&@V6|R9{lB`=ksudt7!Pd*&Bex4%VSJw34f{aDd9D;U zUI9>Uo4(w%9Vw^8GbXOV=cKMksH_wtP%xAWEFR#bcRssYc$Sgv0Tv*O+Dz2p%F1xT z`?|2u7c+vnokkZy5?m&AB{id_mrNV2$}F9Ywr>F38En zA&NTX51eJUdd3neex*gz0>z2d4~qslJGXCfMG=Q0?oNv;!3&LrHkS#^$%`W+|7Z>q zA(DNv@7BMFuA`#SsmqK8M*wU zexz2|UR<1<5SChhZ*x0Y9x|Qy0TA}{>^i>C8KT=5QETcDn)X~G3N43WQ7he6ITXBmP~E69L*HLxGuHteSMYxjP4jaZHq<* z_FWy)8fII=E8u6CyJcv4A(90VD85pwm6ch8Sl2{*O&52b6DR@c!_U&?%%shXNAJ3( zHEaeKn6gGJpa(+;Q&r`qY}6flL(7V6W`G*}ajc!S*xtH(ALy!pPSgLGhZOi;0H-^L znI^+)?v4OtpE9aQSB!(zz_3~sH1`6X zAILX%vegqjmBoZEuB<<<2UB}IXxY2FAt3mp|MxwIp_onFZ6twFu6A7)sIY%Dw`94L zU*BvK*)bu6O~;+3M_~7y&nb?AUOt^!S14;uy;!&vY6G}AK0V<}8JTu5-2CE5K6*g# zn>0WyZk^?zB5=1?Or;*k`vBTk>C$o0&(2ntBcLi~61aUc!6{M5;<|Dlch)F|-vbPD zWz`?rI|<)O9Uf8Pp$F?nzwv9MEGjoNk>~`v@}hY3EtdU#(bSw2&fEa)NS;6Z4rNCq zv3C?i73}_OQtSH=5OfMuW)S|^wkkUILP}TF=Fr7wPa|2MLN+H*W{C;T&)hS@nn7iS zZp4qI4_8v6MhundG;&)f_xjnffWEo%h~0|)W#%&AS?&vv9cdT@Jp6z!L;%04?hdOZ zvLaIGjV1oKOYkphNI3LI8!rJ+!@@~;_~@`4mhXN>9xKmp^sSue(<7r*$*Yaun+bC0 z>V17vLc~OKSk@2r(jaB-Z%Tjt2uAhZ!xuK$>f1s5SJzXZ2mWvHP9RLGlISvlagC+3 zfq{wKS?@nyWo2h?+j;jRVc&q4=G{y7=%dMM^9nF^2aJy{o?q{lN$1V=7YMGnIG_`* zY40rR^=lsHXmxTr0XzZ?tY#F)v8k1W8Mz@x=5 z{1{d?sI0U1`Hq9)z`^-^P4tSlr{|)NBSjy>y`Q_^+&fo%edlEt8wgieSL!NO9EXk* zDQ-v-ZAq#B{-ga8a&vtyU=3>UR9U$s7*NrcpQ&4KWy5}Z2Y&AvEhgWgij z3OwpY!2SmVH^9$zklp)vuQN`qJ;3qf{%oAWR|yj0(c(Fmy>V28ctFsKClc8%9bwqD z^!?rD`sVV-y5GeVEAWD*3Fr6{v(44s9{l}?^eoKQ6TY(?_@a?AgL)% z0b7*^v%&jsn#_iLZdtzcAR;N6UD|+hxlJTSASLa`8Zz2`9SMB|V8$96w%~vK<6Vll zt(u^DNtD&@f{N##vgfO9PaHk(tKxO^UHX%deQd^wnMjpzus+%2edB-bG7C25yw`H! zuC%`&j>E9>hErqlBwgX(Y_eS7hemp^wUANi`)#46nj%=?Y5K@N-lsC6G}IMD_%r_w z8oq}0?nK;Rqvy&xYHju1T?q3`cu>%q|J%9NF6UxJ;e02a(vX6Xg8t7>g(h{+EkR63 zhj*xCqcy{Q&`$$Vbn!lBweVOJ+^7I5)f2GrGROFUvDrSS?qi<=rpk<*>{!38Uy z#GwSQ?($Z!toTRC2VS+cbt{IX53^=!RwXycN8 zs~vFSw#_PXDz7k$(%^Bmsc{GwZx>7^Eq(qn)8pPJ$g^MiekT{_@mfHcOVRsqY4I)) zLhCaVcA}@)%Y+@mo-2A^y}({XaEHy?Jkxvi@@!=am*mwO<^+Z@^=gExz}g)G=iI+O zn-vld^nYdW&?zRe=a+!l7A}2A0~M46qwW$Rew+iv;u~e_=EqwzJOJ+>9313<`u@uM z9QYvT%aZpe7*+tZ z`_cH$n>6?8<=EHoUcMg*DOML8YRk{jET(U@%BaAjtb)?KB8i(~ zG&j=rIE+XN6g1|{1qygwdB4z~#l*x56}W#=wnnNoMJqfJN;v6&&E9|6wG$ON`B@`L z_z2Hv-IBd(TUEiBE}3-_eCvf=NUsoyRnFt|Th4oW-pis=?X6as6MByeBhT*pfIc-7 zBW$v#mQD=*sg|n)O`HRtMPjeohskoAVc>9jWv)RArJ39JK61j(E$AoG8;{Gu)D^_^ zr<2f+nC1hA+#$MFX>$2v=ya1Ro9i<#m_oQAC9RTRZ~fpU;@8oM-6-QK9U%A2kI=@< z2?A=t0LeTc)X>sc%^gKpYy+9(ffDdH6E^?ZYzM3E2JJ-2P}1o5N{txay1)FEw64p8 z$av!%X-7{PVd`Lb=x}8(8(o>r^OL9LEm<~628gAcW+KF78?>CF7J`%lTUu1XfFak- zacWycF3a=QOVbxZ0RJGGdD`H?@fv4C3`67a!ygqa|&gp!*<1j!gFsi!w-xBEAB z&zqy(5<*|YABzhg_!Nz9gWj)Rdg{G=4>(gz>B%dG11n%?+AVhEeNO!p2{eRdy}frl zjgj4Lcf0-SMoR>}`)>kUT-J3_nnHWVBX$B36E+{~JXH8t?$OjZOFpLSk?q{KKHDq8 zasJc3LA>~?#w;c##>4gzcbAaRBy3VxX4nKKM{^T8tkEj>V!SDFr0&=-!0h{c+mX%g zJoncC;kRt`S#MpQT0S}*AZHRAnk!`Ag#!hiiR$~uF8W-9WUnjgWy?H&!FF6bAW1!^ zhnJJP^v6A$^S^(neAdY;daLg0x|pQtEuR4rM85XseY3G)^-6$U&v?X=aD@V*)RUyZ z$(({9jaZQj)-GIm-%Fp1v;Jc&(3w?%b~BucopU!djiN-{;*PP-(3Zz0%OMWV0oy zrX2Kq=#jY1Kvu^B@2-6n3{n2{hw&Mou&^EQ*FuKhC_k7i%yx#?Nf{v0C@|n90-K2L zoPOt8w`F^o$S>pfBYSaPPlF?I$O?l4y;nOu4--6r##UE5R!2$*aF|~C7FE}KHTx*d z8}Fgi&;k(8&Bz-&wlaZpA8&u&Sl7y^@O&HxvG?!3|GE#=qa>o?f2fmE{`W!V+q{_3 z?)0YPH-4qR@j1CbT>9$mS#GND?1qFH?ru&Q3LekOG(B1oRA_3J2fsB}Z1OJg6;A>u z&hKuZNQSCIo}3#KyUI|z{F2Rb`$3B9ssJ?dEpR#2 z!mZ=r$8HX^oO3;v4=D4`WA$V`u(!RhZ}C4YopoH3|JTL^q+6t<@k>aDv@{qX2nu72 z?hZk^q)R|hx=|1r-QC@YbdIjkozDk<4}bI8?rnFRbDisYpUl+a%uH#&hbW}2KPlUS zdyi+SCs5&)^MFbr`~VrWeHPz4oz(!zt{HJ0;XLYxoVTU4 zT^fO`ah^2#1wL2L>TFcNRF z=i74KG6fq`9G92eb!&m9F^h7_S1P-dso!BSTU(YjH>Wj+vySXW^Y14U6HO1MrAexg zl{0JQ7u1YzVa58^oOS;==Fh0zz1IhnWKvV)!|y4~B|gclq)dfRc*O=eFSyVGe$} zBvqA6WGAF*P-C4QOBQ#@s;D6J!UnlvL>UFJ25JoTTN_OtZTS4F7wZ}|PYey1XJeQM zsYXuM%Jn5~&%??ZtAxv@_ay?lM0KU(7Us2H2Ny2!=hj|!3>EUS)gP8R?U&DtwB@V( zD&X<-nh!WAJ&0?HbtB<^&Q)(oU@-otFQ<2FyRd}oPLx@8wTI5RXjXskeYHnylk@j5 zbTyrZ^9Y}NU+qVKODc|9Rp~!z=q00uP93BgohA$sQqA1{b#wJ}CDxp>?2LZF(7wUJ zxJaq&FUj`1jb{ymfyTL9tyVosgJ2Gg@bq4$8XbJ{rRsPO@ex`~FQIMJwH1_k5o-&qtC`qTn+W=Ih@x`_=5}6= ziocs7eI*^-jFUOvi4LFQdkv0qAEzuvlwZqod)yO?o2#hq1L0*<<$BcYy1^5wqm~(NI z`|-8Q38l|Fk7}Kd0mIMY#;hc-5sYHS4(t>ojUKvYZTIy`eSLIc6^Y^37cPuHp?reP zGWLPa(hoIq)U`#1MH8|!oQ6k+?X}+unH{qx3Vrm{_k*vgSO_}G8GWs=#MH{(=t~75 z`6Ld1Gv!0CuYrq}Glxzwcjw9Z)r^eq+G`aaqK)&C^bN34a`0{T^RS=#Vv=>Y28J_1 zc|QLb5sm?g`x4__p8P7|>@Fj2)-aydhtPVWoz_gS{O>RmJcG zgLUp0qg79{$?lO0d9N4LJedDxH6=Zo-0wy7cTc-+TUV0C;M)Xh(YjCBW_$Sb?0HSW zLKbn(Y{mlUrq5!5H`@(mOarbV$x;#^NmBQv2EA_LWEUBAr<+^o?4q-ev>S?bJK7~~ zl3GnoZZXe?7g5^=E0)YN@Ih1rNEHVOf^2af8JjJ!vVF7g97`*! zcDZn_G)614Io5BO#yDQs(qwB!2A{==g{8;yLhqK6DK!&51}5s=iCatfvoju2lWnIa zdW1*yS+FZ6x&9QvyZM{y-nnMGrKZg&t^8ETxE_|Jd1-ik?*EH z3y?10?x+UNr=Qb@yGNy7GgdE=Y_5U{9)YsSNl8x)AK_W(eH?pUcNqp-FDD&zJzyH_ z%=(Z*sR{I#-mjY9e%`HQc9xT8P3PIKzPvCV2%c5{8lvX58Y{4b!7*9ldeUd>Yujq; z1V%&m0{W15<=@6e@E{d?+k(d{Bugq(?Upu6kZ3!!n34or7|;K>-hf_2{sKZ> zshp!iu74p=ejf`{RNkc@VTIW!t7{FL=~R&Cm5dM8tCBU`kb@%{rGMZclW~+26@64l z{e!RH&|F?_I&*9flZHzKB;B^(o#Ldkp$GmadzFfux4B#w;E`6`H{F?Lf zKPA&ZM1u7U%&+zlPvxHOm;zcE3$s)oy)(tnQ*5T=Vl$&D{!+mv7P$ci>QgRxB6ahac=>O5hUbGIJEy67ma?07a4f@i&|5D43Ji3yM3@cR%qgQNNDr&>6_ z)s;=iFn*xP}t&C$W|A8=nVu=tG#DJkY3#iu1a^l&$d`m7u4^BpAO**~DtrB9% z70ksA-eBhp;8d3eaiN63zBF->2!>Y)ez6Jkc^ z`_YzdaF8V2J{S@2r$mh9Z5JX6-Ff|h_$uhYMOL9c3Xy$9$lffI0n|1SGw_&hAuAvE z-oT5HeERS202Xxqi`#EPF?Tf6A#8~xzhv89;BU15Pi=Eovgby{PyzsL~tJ%s~?oCR(_ z*$&cHvcbAA&hkfm?OB6T`|aZrjEsLU>IA*7W*omzy^Ubw>=$t~O=akA-F39xSzA91 zdT@ZHO~pPwn~2*rcNFqzi*1Ds&-$I>9t%wUdptbNt7E@cXVCr(SW@q;xht3n@|8ds zH564dGpK4)N*3&HBjgUX*jQP6aOR4fUpIZ-Nh2Bh3FLdTfZ6}vN0S6P-OfbUIZcketG9 zg>!5VzjZtzZG)fxkB{T+rwKiq<(IS_lREHo(;O17QZOCd(cVs>gRRWke)!~}OhH^h z2U4vGM7qA?TkxH4Wr^Sb$Yt_b3TyrR55ai7n|xTo6)4YMr2OM)Z`Td(isL!pb>L44o!IrVZJYqj;UAzB zVH%99|2LGsj{`_W@+5dRddC&dWZcC)r}P|Y$%s|)KVoG?^X4GNmD0ynLJX{rOC^Ctfb=BQKM07 zkb6~+y$q%dhHc0iw$U6N23;AZ?k|)=VaZvl5Pena%+z7*GCsaiq+)-+OooX9oI%&) zXl-sZXCXPgNon?RloIu9{_)B+J7a3Ug8mC&Vs8tM`VLIIywR0Y8sHm`1-%INrjS{` zxu(`}sP$wK+%GDmTaJlomn(>On6j?GZ4A{Pho}nP45g8_m9-CSo2nW9ZF}F!4ISj? zbHqB%+t1i*4V_-Pls)=eLxYaK8)R&oc6WV^%D@PChZU-Thm{g)0_QzN11}v zzVu?B0*l;0#Q+$R^5<^c=IE~;pq~z%d+a}H^>ToZ4GnCut`E7CDUBexpM>NBS5GQB zDm;J3_q3n=n+4VU;JEaWa6v%$yr9qUGves@B~|fbP;Qiv7atI~n+_xyphNlFmGZyP zb0!7BlmrU?CcSWT(4O9;#)<|``H`H3@QD~L-I-;{%fnwtLlE&L=2q8;3<|;k^i3vMesNzvO6oXL}ty3=q!w^_OT5z!aA)_Vt6fQ*7*RQSYH0= zsSCvmfGEH!dEUI}OrnKJ3@ex~`7r+2>rW=73Rb;+{#@C_t;qMU^rMcB#;{$t`-?x} zA%cp4;1dhuR<(eYu9FjDqj|x1u6#BnIB53LCI*}lVv(ETeHz|$vQll z$zZW6Zlq27mT2rxbJaQ3X&L&lgJo^by)ujHX%F@g0?T+dQKSK)3FPEa6L*Qi{PnYW zrVd83*e{&ROa~qd73f^$5k)Ke5*A;Nb4|vyaoK&Ff#j&^)lh0z{5;<`&^%s0Ic&$& zGU*I4;4@0NJpy1j#;n1X){m>b>K}i%qV6aJ>C_FkK89<)fF>|fhfgq4BmLnJaZn{0 z*k6e4<-24Dl2S2rh&H#TZ?0`OiBrkvYnF|VIoCt&SbfiEgtTHmip>?ieVP@CyF=vi zP-6;WFW@{I*ho;ckMcX)YdByFPD*DJ`)24G7H8`&#D!4*fK|GGHt2c4RHvgVrUo3( zs$H28JZZ%nS5rfAz1u}aoA$VoA5=>$Mlm$&T~)Q&Ul$jDS*N$=3)iais1^7^Z4>ZB zf3iN=uqN{%EoPOhPmqROEjm}TGil_Q1{1_Jpk*aDAk4*njM^{X zW8wTqe;L6*9fhbmup0d3g|fPpKtZP0k0U_kop?nnL0K`6^|LPyK}=7}-GSYr)XmYz ze)Y8YG|t}OO@XvinwHpBW~~(v)BlUxH>-5|)f{O)MBuJWm6@D^&m@YrCvq^U>2z^O zDkJBPo)btUP0ChqsTI5+d*4J@%K3vEVkpGw>~T+nUE``7IHonDOnJh(TML@wK}~Q( zss3S%LNhLd4{>Fv&AI;)pmhAn-o?*iIkK7DkF**S(Yu;{EY=RIJthuCuis^U=e+K; z5S=Q@`=b|m#;{(p*QA?iuZ*T z@69&XTvj7Bbk)gqMN}dJXR1COd1MjOYV70!&lHXSX)2Tua`rThMU<)v9CYV*2JC?1lUTmo4jhBhweUhWfpe9`e~oxMaem!B*NcCq@8IZqDv)j z-$@%y=^4BAutKR=U~FVa&i_r~WnK|kL&N;M$?$(zB~ml{$09~A;rJ6FA#WGeBwfc8 za;HK`7}+5&dbZd_*~#v zyzMxk_t3%?vL^%2@Ze0G?URX#PA3gXl9ly!$6UUO`|%c_2v_O7IzLjC4@C2!3(9$F z9Ptv4oI`Ize4WGhvwYfb7q-6b-WC+8k=bEH7WdMe#`uIZn(_-?F$FAD2hh^OE;L3R zobc&{e%llaV3m}5oV^#I@PXM9+~0SUo0rb+=lXj%uOo&qU*>@_- zTtC9*pKt8s3#;jK0%uRdwz&%ZXYJAeFy77KF~lH4W%mq^{wDUi^9!ZbP(^P=A%DGP zYS2DTtjH+FY6uySa3ctdsBsr`?dH<`vpU{()V+N~oQOL16#C-55mAXf0?nG{r*oUK zue9fUyCsAgw(0ljmezg4pmB(ZQqJUaEWjx|d-mH|e-+O#3NYB#wV0S#a8qd%5*l(W z)>~rQf=ve5tva}~PP&W->5g~OHp+=XIl0aOE$${L^&=>F`-1QL*9itC$W7T^g^s#Yqw!!N6G{?5QkIUTO4ccB!`jW!)MPJII zj|0_o1T|KpW29|~r31nWE(x*`)O$ntUpI56_P41SGU%`Co#meTT=waWW9B7&97q!2 z#W&o)_3!;pQg$+!8!x1rBQ?!8kSI^9s_2#%1WkVFF*7R|%4D5*)V;}aY6w2ro_#hf z-nF&eZUu9uliOa*qY>+ff_R=s_fts7F@olH0_f00jB3M`Yv%_T9ujoKsg!JVw$%nX z4FfMQq%Z-vIgGSC<0a!$FUjxJ1^(4VB|%bnZ5!)DO&Zr2h%tPjek{3I%IJl1G3nMN zK+c*=dlTp^5u_ptutKpQg_JahdGl9vGDhCc?(5iu&Qo345VBG~3r?GezizHRUGYTN zEKQ!~m+}EeLl2tfiBFuIAh}Mn1Qqs+?y01W%c9Pz@{8(r;^`Fbszf3T<5Aw_uT2M2 z7^Iu`!wkUyvT8v>L)XJ5qJb)dj-kF5c>f9q>|3oKT|YJyL6ENN5-0v1*!-{zDMp+6 zWA*?a=?}~MsN%L-HQusTd__lq&!5X|X4(zJY8g{$sX(3YDW7o)<_>aWY^?UdH2-ur z4>-?H+}St&?CZW-VGe+r12%PElsZu)P;|x2n+eS2N&?o&P}A1av{vhg`j9|8`b89u zAdS8ltQBn$+5uc`6MGXPH33(pKB>K6p(~?_mNHGtxuLNqsDLI3EN27Q@EDJinBtR; z0Bq#!sd71sq(6X(yt}C-v1a%i1`B^21Y6O7N*OW_EOmr?R$+DJGvdvz`NhooWvqkF zrtjN3t}`Vu#CXAl1GvtXPitE|RGggtx)Z`!b7Z8vhs2N0b!CmK`3qqxPJsqn)+Emr zZ+Q2#s$ZTekrrjL^#F~8fKwexj2NvHM9vuq#rvW2RqV6si;PFOoHedD3A%3r5XV^Gm&BW}i`4PLeQqzt zI?*?U&8R1pR|pFVjd|9$03R)||3@aOVw=jrn3(gMub_a^kp8@SvrPLbw|MJY%u-)- z9;dyJYWAv{rHC&C@YuKcEHx~M6LDhE%%6E)AF!lM6gL`YdF%4* z7fSwd2@PR0xf~g$@%GSf5mv#{b@X}qJbPgXQaAJ=*?Mmk6!X?2qF6|Efw})*qV~np zj3n%JmF)}1LCRJ^P&(Wm#*Qg1_rn=v{;Fzb0&1jyy!@l*Sjgw)#F6E#ei`Mq?${`J ze)k0^hC1?d-g9bm5jg>}ywcu2Lg-zI7N=Z}W{pFI*V}XnrTc1M&(c3|1b3G$#zqrE|=n3G5sprbv8t}l+%QY4Y;*pA^x@mP+D957|!lQMNCFww~44+aoGeH|QNT-KK$ zBjBLt{pmiKBK98;$aGs;iDlO{fDx>yzBCT^&3jx1JmCAmG9)r~6W^eb=GGm;`laNG z6qXhpOm-w##*cbBZ%3g`e6e7`PR|kcXY>j?abTMgni=r$U^?i}d+aO$c@GoN=n`|A zoB(ZtgCSR@LM8pX+8bqMGKW{AJh)<1pe-jeGmW+=4sj&=rkoI%m;d^|C3mNXx3+k$ zxkJ3{B)Jm%23JR1^4ToZ+U z5v@=cNyJ7A7XrL>U6|LLKpF%kmRf@2Tbeh-&vGWE>FBv!`%a0Q-L-12ql!&zs`%Hv z9$Gjnw^=yD1uEGpRw;1vI6IM%#ZFC4A#o~;i#vCH6Ixca$$+DWd;u@NyJ+9Nr&lId zpvM~@Q87A#^D2OAl9;%Ns@|JNg|>)JQZ^*;j7EFu1p#%!+^!1lI{rGmx_VN=C4c8X zoP3gBs2u%`PlUYvzD}ud7OytAbSUa#Ld}2FwO#7tvZeVrW=`kr^(a6gTANF1CJe)j zhg2NQZ3fhcQL?xQUv(qb@a8bwms#A1ia*c4SUkgfxxZ8&dH&?`&(xsK=usy=T}W;y z$uAhz=%nat-X$ko%>#r!AyL=OnS;B35W{WG$LWVxT9{^9n*l9*lRZT0A3Hj(CUsvo z`UNr)a6Jy49<;dmak^Xs3Ukql_c)G-rn$Gt|FgQErNVfbN^}AZ+qWM)-5`ZBp)}nwOW6m~=+#&R)g%#k zz0zX%|9^FvLLqCsSg*QUAs;d73aBU9Z2_39MK7`z(rNS_Ng_ku|B)LU;!|P3psEzo zO&3N;N6a1g!j@qygul=ag#p4!ElLU;=-^Lv$DT=EZiVq7NQD~Olj50kwl7EU;4+Z8 zaQ=%QgyDP0I{|D91EY^V`gej%6(|M>&|!y$%Cs!wd2|&9qF-ECv;X_ot^dTDEzQk~ z2_aZt=>psziZx?4R;51pkf8VdzPSo;=vUgCnH zvrjCo_RZ{HOWn`N4l!oXe+vwHa#WK3%ep$f)mT>mx8rWl(9rq42O2tiNUqOMuTEp> zkN92$#F+21701NHnDi%<#C#JAL6BDTTCf9kV`}1GtR)wzj1C>K#DG4g8ZkOq%HH{` zK`(!qgVRB1N*z(7pZ^~yf>gCKrYUU#U3dS5)>UUbDfQ*yl!g~lRkrgQKHue{LgHWM zpo=vhh&JQ&;&J@iVhd+@=tPtzhE)oU_~+#N`^_!qEKtbc#n%iBBQthKRqASC6FKW?EYgoRlNoix z-Cv*$C`}p=9fk93hp|HTJJ8ZTBt&)mj{%((=VWt>3j)MH!8p2Gd#(-u9o@vyaGCM3 zOauDE^yZs9*a0zc66Y*7P6ltk`1vGcE>x zw}x&mRi1XS@58d7qImuFE$!mXjr^5<`8+^VL0hb_!Q}X_@EEv@ur)m` zc~1odT%`9PO=}!yjP9R}OF;X$Xn>Mm!Ec2C{Y;~6nU;Oh#Qt~#caraQnzSMVMtxcK zzZ}~J9tv5;BY1{B{Chkd(L1ks`4#@{9I{iQiqb)zf*YxDdf%Iv!9QL5JO}Pb+PEDRyMv?!cyXNWf2oSuuHn{iqknW16ESK*BL z3cmv9=RdSK>ub>lg=RzaS0A8%%Qv~J$evMr|Dwo2{jp(GUB|es=Ggtg9jHXvFn<3K zwHVw`EnWIiuq1TEtewmF-$Q)IAyHI=W-w?{FQ2#cJv#;kD8MS}T1HYwgSp6+`_b9u zspXy-8tpeZ_Al#ZZJR3SsVTF4X{u9GUH>Q|-H-2|bni8`0??UmmuImnTJyuqh5FyL z)Ia8C5WauhWl5LcCcM=;Nzox)BcE#U=nK*^3n$~(_3WvJDAiBMF}Bc-M?QFnq7K4< z;@dQS)`n+d>r?!aA^#BwR25{Ucy|e@UsEh{izboLG>TU+&NTAf?tmkB#&{26m7UG`Et3lTNtEKTinKlmID3Vd!tuW?|c|tZ|`L{qw##i6ZRg2zef0eJ{B7P@Mni61?%^c4&%Ygc>x zq88r&eUDgZJYT{+fM<9MW<26#66nBTh37jqCjyxTjBnn(#DFRZ=oLSQO0qbRvnPvB zbc)uiscNxT$-PwY%j!alg)#+4O^zAlNV^KyExC?7Y#Ga{nh3}C1EBXKi~vZTvn`>V z`B>6gIzF)F9t*EiFL(>`{2S^pHt|a|TjJo7mB;8G1R1o?)fz}B#5^232wMJS+Tof# z@Ba2repmBv&p(FK8Pr#QekyQ80E_ywX{$Zq->D02U`53}xA1hAvvdK|p8{5ak&d74 z<=lO1EtTstC?f8u84G|T_gC+AzXl5A1I4kHrrYrC8V%=v2_$}%we(=WMskDx(G!{c zL?Ac`4qAhuAx8m%wBK!2-D5t{)EmEu2hH;CHs-?^#C72ZznJ^+#_v21PeFYRSxJNc zZC>zRoq5pbYd24*dB>pVR?KLR46IInvZkNefoBk%letVVTgb8P%+M|~oP+Odf4yf_ z7?b$)82xjx^N_Avj()QFTGxIrb|KFUPON->rKRM0SxZ#GdNmz|VHEer^I7_T)31H6 z&#U&+xFhe$f70U~+4G=vStW>Mm*C)nR=Bq+D8;U-#Uk%Nnd;L84Lzpl^2V$&!uiXx zEdA->E-UwrM_+CN07eA2V&bM_%rann4Bn=vP zS7mEd-YBtK_n5Kzajv8>H9bN zE6swcZwc3Ns=7=r+y;sfZ8JbBQ44bSRiAZu%>M!^WQdpL;uw z13}u!g7nx_cjIq95&jR1)z7bQhk)Z@1gmHcaJ`;T@zDd_X#Uq)g~BiXn=%xHxwN+g z{PO+;=$2yva=kPRRy!BA5=_$%7^O4(I?ttC{fYYC4I55+3Q4GG2D%^fbt1pIiI{x^?{}0*w?Kk^qQs~DIcHLUDQxTI{G}D=5Exh0?HK<6 zPT}{60H4bh?ay;p#Tm!40+8B@#XpbqGyTj?B2!Nb1H65|_S#9SD?&7*k~{Zt06&6~ zlC^lN_XT)$P~Is*#-;8dSzqr~0hKuDD0_yDE&&J6-36>+1_SJ>U$B?r;m!vZ4!S#-|L{>R__8#Bdrzj48BsQ#=--&iYu8tA2^V z|JIiXi{U4DE7yTV{sO?-?6$_rV~$L+3>KvMUATqE!jUX+qg$|r4GA4 z6-zpGux47OB5)SU&vtEe>nNOu+2-^ZVb&t{&1u1;0pnt$nNIebYiDv){S8kfJOfn6 zxLpaS8mw4cD2|Ce1s4?~0siC3(bVUeuvF}ibxq*L&v2A6y;V3Bz8YuMXr|{4QrYGT!a7GpTOCi>`UP!net<$H+NBn!f(piMpMVkWsRlRa+l!D? zC~wq#&0S6)k!E@BjegrzJ~7M+`63K_)Q^ ztpf7$=`^bdU&(FS+&3Xd#i(%qNNN!d_1M@y>$4Is)qVbh;gWeS7s?+`@=iHqXq#S| zXVfj|@2Vp*hweY+7N9!3{S0YA&fxsKkhIuh2=H1wT=-7lTvf8tmpsRneNMrg8I16>+o%yUXSjad%tOKDx3EAR_v~|3C27qP4$RN0Rrm*SV{W&cP$ozQvnTI36dx`)y!h3(w_KwGi z>1x6q;_K*JXW7A%3x=r#EKq0usbW<#Wznn&`%}dOI3+~Jmcd)IU-0;+|M;Pb;c_+j zWV0jfb!*pR?wAPY?x@64?t}4`V09Po@kfNMM=&qoKb~k*tAvizeM{tSY$xh7!UDu> zg=KrM4=_yLZmK;@IV>JxBsUU0dVwxeQ^(Lfde&&9^ux98SU>;@pN z`&9_UNFK1W5*d2kMP@e^JO`gGYZCZN9+8l-In!s9>jW@BJVCz5(`xo3(XeQ)mjq!O zyCv?6SLIbn>nm#@NZ9_jculU=W)*?6Qu&A?^YT4qgfITl~U3n62lNu zI^d7*elJm-3R+f7*nohK;9_`0KF3Yj+Vw-5PYG_MYWEirw=?Yi+;#zbM=00Q%yn~r zX8%%TeN0x6LKN;lH-`Y0ernx&p3FUO4rlB+UQuqt{uEI`Az1+*-w9;nl6V?p6ipmA zFI*?E;~ynQ^qb%b)~9!PZ)Ai&JQ^B^*~M;PLd>F^7O|w&Tjyo&7CXryw@=amm1XPZ zbO`JL`sFQdE-|I`Ur$d%{!`88GNcf2zaGIs;5f5rJIx8i&@B1Pc7~n$A|nxPuRT?!QQVDz%^B;c-8=8z)wo8`NTx|df3wN@pwjR6c_UAtv(o=d~!?zWGwt7D_(LSF{ zwFj#dr-v}W(v0h=Me=))XFTlH@W~csOjF&h#6?FLaIoA*HTAHIW3KSy?FZ%F;FtS@}6nLy-llVIDS&Z%bZc51IU;!@CxmAd-oZmbsbZB=bykOE4Ap+m%Wtf?Qtb<_KaMKCS2-`d@5lru z#69QbL?&?Z?L+t3AO`B;K8Yu0%X=*ddzk`%F}?V$hGb7FbAwDjyV6&HYUdzac*+Lw zCjvIBN2MD@Io&BEKLSKeJ6>s-jHFM5J5$USI=!T71xR7fD<`A|NojCT`wn7!)t)j= zkjKQ2@DT0+!$0 zU$xR_BUKWdx|Xz-V_ubdd6HBAVM=(mG#-<1s5$RWaz(%JLf|%9bS?D+po_G|gY1Cj zrvu~6O4$lfrweu84^|$+Fso(^%1MF7dPo%jiN~j9SqRhKvD6U^7bluo*5htqso~R_nIYBnn%MimE+BC z9;=1)xT%So2L%)fiQm9*tbFrZ3>#?}=Acl;Qd^WUGL3Pt1B~s6laG2~MnQ-lPAc1r z&Un^mA6&Dam5Y8uK{SqkEYXg4Irz=9Eje)qIekSKMz>$a;7atjz3wuvK2Pl)WGa8< zGHv62UL$bNtE_vf``pq}+m1c2xUkW*!NLW;r(rwDN;?W^0$S(GNiA-f>(9U z_t@f(+U2m!Aa)BqmieZDeLOO=jof1YW^O|xH%6*k#Vl*@)OX&u>Jw2X<5Z#UA#4+L z$FnMWF?7QHgwEJtV%nY+oH-A=sXQ~Q@oil17l_q^WsZL|u}bZ=!6U|b`$w`((0Q3E zO3;G{a4_8?>UjQn`I!^9bzf>%EkyQW>e|%?Ra}%Dd_+5t?q?c)KOn$g@IA@T^f!#riyOpt$0=AQ45>#MWEK=t| zLnjTE(GYTrvA-5ZzT6961N&PIhQ=JMSUOVP`gl1%Etgx>+3ne?D<@;yR~ds?(Rk!9 zmSp7}|1I{Clu6Dl6L~Nw&%M1qJ{aC6B01@qalLZUQiK$Dd-KZ?_L1dFlKYn1MEN1f zI?+(s)A5?Nqkj0h)?rTAO%Hh1egg>(ptsGEGYH9eT)sE&$@ zKX;^CIq>)nX?`}4d&2GG~ytyiD+op7rmC!hM1jD4Z0ssHai^U4WL#x!^uRD;o zI8k1=J^sFA(UKR1+z*fe6sM6yecx$tiK*`;gdlu6;Od(RqZ7a{y$?8fuzV46pA=hz z6>v^(hcDu&_$FkRI|0Si_J`kGA4XJJ8=Srzr*eXGx#+86!O_u>Ra&tN*K^-d(=>34 zt5;hd9m3$wu6c3Cri)wk^wp2kw;WY@#c%P7?URYA+u0`D(tUYQF;Owp8VbTcNKI)f zn+HA^LRL=AejBriPn(@1DMEQzlam4m0_;eEN=;eHFiupoRuv(FHB$2r)L?C=IZT0{ z@eoswpwfOC$K!;Gfv)M&&d?7aoPX!#AD81~*Ve+W@uVRKk*TG#fCmeE${mdNCe6k&1)@Zi}IS)T+ zA|)|5XZmt^UDkqYOK8s>wQR`Qv=4rR4UC6gs~DUwN~D^=CWi@Hz3ZCX9qHGZBHgXa zK|~z`8B5m6=#=9%I5Hv8aePv@2a(y%A;0i-$@vSq7`O+i_VAU%#H)K-Dd7kw<_t7fKP-*iFJ=fofl6VmJd_4bif&*)a55m1feQcw-ZW03 ziqmSaB|K;UMU>pj-zgpdN^2-92&G69RPpwnoAXU|8Y4~!KPl+$=BX ztFxU7LMfB+m43V{zCS_&g}!8{Y<1$pwVdd&v5k4w7nG1Dj`^WlTbpp~OOUcmBBL<|%Z)IIwOIdp{%mqbH(9msw4Z=?rfQ z;Y%LNO&(GEm)8Aelx1zIEx#dP7zYvCIW7SBcmdCtD-6;iKxaMVJCiRpVX^+V9h?7v zx>;Io4ZD4Qa*jln&tZ+vgKM0{j+11d-je(ow#Ik;5W=hV zK{lW-+h7tZfZXKiYS16Ps|i1u?O|uIJ&M9G4$g z2RG(`6y+w5Bda&=F$v_eL(xN3ZuWZjjoSs+C$z_9s9)$bQ99q{6ZOGqR)z(qJQFG* z$|tZDcjvyvHX^$v9rf)JugLx~x}_yGd}vSygMT$~B#vNy?kAS2&bkZ z)1PX;UdR1TL9DwSjL`UGGf+92wetfU`H^w0LAAN&0X*2TP)f?(J4|2j5uQt9ksj0M zJ^eE}svfK{O}>LwG<+)fLT69pIjKy3zlxqmyiCFQ)FK^7SLAX@E0U9&>?Vrb?5X4T z?>d#ne!TS;>R|fXL&-Bs`^44|1bk1XW3GHi&K9}Jh-fX71OW0Qg45s3|IDDjsErAb z!25^QQCXcPg^Z6m?}ltS|KcDSPn>qSe<~%O$gas+?Epq;7$gDZms^Vj{oi;*Y+qqt z9W<@!1Z9lBW9ek7xGFa!9iNLWtj&-zy!1B)7pv0eqr8$o?bs!=X|l22r8Xlgz-E$C zMjcppmWqw4Ml6+J3fI1MT49TnxD+G9o!%P z{kwIbhO8XFTBW&8F@Jp+T?&whz4dHB$a^RV-1QjF+TkYHW!}}S;hg0JmJ+{m*<`8_ zWWWC9CQrAVO40cTzDf~9`NlPyALy{FsWUob$Z|09KNM_6MKX{%r#V<<`k^ae&&aY8 z0O8v9-Px|YSRApo1-|2mV2JcGg|PAgaOAHq)M}MG9F-Z^jnH}vwo9v0&%_qfdsw&7 zK_f_?`iLJqIf;Fx;d5hJ7x8?YXx?jKzp`bV{=DwWj*f^#-#H#5_wX2G)9r)mNTPLp z35p*ujAyWW)58ovTmOo#-8x>1<1e5bI{3~ycUkx+UD>>|?0&PNHlX8=6xr0nT z)Cb7)(X-@xb4w|y6Djn4-xWw~$~E*vYCo0Ou9r`07+54NY4D0(1SCT5giq|`0Tuq*n&-p)w%Yb&662eAQsbMALef*zzeB!q z91Oz$BH=jsP>4aM&A=T}U1#U3vWw21TlPAdK7e_J=jjx~1z3z4&}}oxWHWr> z-|^7Zi~7Cr-n!U_My9GSO)}kDMtno@Pfsci4W?*E@}8#S1LVkiRn@^E3_^qG@9Q$j z8d?k?lwd0oAJU3{hu$j1X%5@WGKw~Ch$=EP6l^ISi$#~9<-VQ*3zUdRGH}=%-Ua2) zT*tJx+DKTFgKeq{LsVPY8T;a!tSKxnGz{^}YMEg^g<2?7&acF02D7n-iFVH|Fkp(8 z0Z+OF+v{L)A%n^;7IG>0*OxknQ}arBvK-A>XqW*q25%IUE8*Jqi!-07y9%B@iEto) z7IweyA|}>GdF*TF7F?@ShwHwYXP{}6bgH!G=8hZa5R)sb^c19q7sZ3&nYSS8=fJEu zHVlrJsbQ&_EI#;-wTA^ARAlzmx~il*r|(JDB@hSgM&bHd{Lk_6qk`>VzPo*P;xn5^ zYZyi8!4uH+&M5w2n4kXLfKn6gwFh0aAi!BriZ~|LH(k5`zOrsgc)&?#eMQd+HttT1 zY2@Up2w&ld5%AUjSs&Tnjib6wth(1Ku|!r2}}y%k)8&!H_KkTLJUp7t|)CA>@cPPEa8-K zRt_#=VX$m{u+ccy@1>5h;k=#;TgYbYV~2{%UJI?Z5m?6uU#EOAH<}^oGncUKYqz7P z0V1}Jem5@pXR!?|{{3;n>OWjN@KsiCLCWI<)_0JmkxGtFXmcROnOK#y`*~=$yoUl(Hx_ z=6%ufv}&5cT+5Dbki}cF`l5ET-_6kqBW`B!IU4(;;1(perq9{C^y&$3PvVgD&DrI2 zB=-WlT~pIILB0J|N{xES_z;fLfeTSIL4LEP{F;1KZ%b+sJUv)`g=y!xtL`BEi5Pz6 z&$pVcPC6@w1U5nsXnzRdENDm)hW8`X3=$>i>oyBr5~%#q^}gnjrC2_v=0x&@b_sQf zs_;GvHPTN^nVh-x5Ico_`dCnlju`VIod1gu`QSQ7Pw@$!jlYgRrpsK_yEMD>(at;a z<9rIFzH+dvwx?qsr)qD+j64O0c?P$r>Vj~v6irY_rV=_3^E7KxAE0w&i*8&zXd$vX zY9SknSou7;b3ffQlni!bb5b#>)^)uUyYcQsZwP7*5(6I9Qe!PZfIc@B=976s z4SgYI5T{3qTwnlo->@cHR@S$&RC}(=Y`m|SyD-}LE-J@j2QdnL#Dde0-p%fjTn=r) zs!`wOJb^otsz)=JU3VXd>}|VX6MF%Z1=t2;)W#kH*~ZBx*3A@;=>Occ1-g3e z5JU8Ep#Z4&RS;eHe4QD099X+1QKt7v>Ul$c7Lna-Tqyd=RxDpywtCmpGILcE5Z%Cgxj*aZ*qf;k&-L?L#gy zzCM>6Z8MEQ;tm|L=v=vdqhtm5-Uk;WI_xW4?EMNyZ(^r<1@1J`+z||$U%*c25XF$# z`Cmrsvk_fe?V%(P=wVE|f;YlriR?of{30+JNhVqqG*~BKg=g@MiqsZ~zdzkd`Q)A4 z{I&{M&$hKCF=MAg8+W;N^XsN8VE+Li{m&HYwnln;+uxS^#6+bvs)zXDsA~ZCs(@{L zOPs!|7!0f8{K9F0v`I>F?sh}^S%C!W$mQtFwn{}n>NU~jxkM?$AIz7z4#%XmXUxEB^!~t%dJRHS2yKB!wCxpu;Qz zzXu)G*aVv9uh)})j3fl%N~bFxcjhega;NFA+J;si&$c@@7sS0V%OR6Fg0{X8-hKIc zr9luKnvR?^Cgd*?DW0pHon1>*!75g1cr0O;<&~1muljan)|=qdFM63V(lwn#;oJr$ zu-a4(7tdCx#>xJ}0l}1Z{Rijod*56Nwg&)kU!SGPU=wY%w8aS77dmAMsZlPr6#{9V zUr+}bFt2R4#5u#Z2~4Od>xDHZkx6CvZICxb>gazK6y{j@s66`!F4zp!s8Ctoc{YWx zbVavC=4_n4cxLA6#i6CIG|L&+9R<^gW-QN}J*oZfb8Z+MoUptgyRrOGGPhTT=)1(F z`*JG>-lv7@4HKEikAcgqv|zE95X1v4l6*{-7M%kiO`PZ9^)()f$!PGk{XLt?wxZ5O zOm$5xzsA@zf#ueGpOnighqlpOE_zm1qR09#W3=ilE$j!Fg!({3W)tY-nBAJ8Z#=iQ z(D9=CVtFlo1?XrlW)w9cRz8IoH8d%ECZheSS5YkMp)<3V$=e%|{zzZ8A?RRX>sOC| zU-+o0K7;+`N`Z;9O4&)GR`zk~FQZyCx@iJJ&vU;UAn7(ceW3T;7Fu&>I22QZt*#Kn zyq;%W2-HZL##G8{l(Aam9>0SyOJ35{*sQ)=?YOQIqt>l>XcK&|jNVOYyzp+iSx+`} zy_DK6xlSUgK~cxSD!qnX_|2V@EmX`VXv*d`UfyU8DdYz|7)3Cn>l%W15KX z%TkHt?e6|6qBMJy@R8$3&hlZ@uni>rmF$>{T>eb_!f;ob_61)K7GZ}K>Wq+g7`lOO z;b>G;Qc3`&cbiyremsDq&k7*JYFp9EhS6bW@~U%WhR#=lvTm^S0M7|6hlok@3XzUI z>aL+99JW?q@2xZ`zL9?T_di#{G*%Ecuf=;Nit%RPNC_SJ^0!L_u7(wl^i6usDs6E?j!}05{h?T#JNFH=s;^aXDU(jP8CL;gdzl+k8*- zd}(;HVEfcj9v&g`J;t%ZMLrBQ*`iEcdPk>{HF7**`Z&%isQU4Omib%`a^qtT<$5x$ z@@zj@ZpJd^+3M!4aBbooy=YXImhx*~8c#KIjQWpU@xL+~pX@z#&BIXS#nEX)&#Ipc zB1l1wNnWGtVKpfHiBnuOGDygEo;i5C?XwSY6&NEPCvRr5|E*ElfDvqiqei9+%>3oH zLYeDR4C>Xt2HvBHl-;>Pfoc3EAnIAE-7jH8qwyzaF!v~Pl3dXU!(DqU*l!qLv08&sy>-MF1vkjll@}`LB6+XXKa#*>>52eii=AaYtMMq z)L}ssZG4XPS2wCvluu@)IOmY^+0nrQL<2sQ$fJ67UuzU{1Ye!@QAK9k?OMv%PN)jB zG{uJTYTdV4&1$Qme9KO%V&AR5aQ0Hs`+4eQi5-(;`qwZB4el?Q0AyKio)4Qp8dB5h zmL2;I0^P~dGoRM$30B}F(();2;uYr70#RQgW(y->A)&Bgq230#$6g->4T-%~5m?cv zccc-=K2*tJ6l*!n8nx{TH?~`xn z9+9@@51DjmO0APBnUU`;T-SY!Zg?#4!e1^=w6p-^jsI6>lY{8mbh87W%1zmYxbPIn z>X9C+I#xAok`%N@ut-b~YE8P2Lz7~Flj_?fstth1PxbNp(d|JBYO-aNO}@^+hFN|R zb@tYOTpZbN{&2&?%I&ZQc^0Nz2dvE|>+{t)afqi~T>uQwv79R>8*SAj=Id>8 zqaC}V7X+cWUz}D7hn1~c`@a*sKd7zvi3HV2^yPNbEJ%`Yd;K^ArG(-A5ArsHJhDLS z7=Yq|Rzt;e0H$)TApbhS{bIMXb&{;7qLx{Zc3J}7?`7Rx%(q{BdlLr=+MX7=GSh2c z#pvc{5>V7q+XT*7Hd4bGCPFvP_n-ZeUCr>#s*p}Bw;$M;=T@75R-Vpa{wi@#L~s*X zCT7dlWXu6y8}z#2Gvng9cb$6Qh>-p^xn~=vQorUVb^w@qEe#FL+U%+dVLlMj-Xb!Z z`oBg^_O+q{@su=9aFTk6aW?LS^A?roalN)&WH>p=9<3`KPHwpKH*`}~%w+?GsB>lu zF2<%_2V9rpxJKmL5sq9py$#RYABtFJMR+17qTZPj%V?F*E9`*=Zf&xwUe5*f2wuoO z_7Hw#m+^65ri|)*>6o$o)%gJV!aYC5R?kKI;CcBk-@SH6iA$V*_;pQ|vKEa~Og{LJ znjTR5X-i7`%r)CYlIhwt;^dh8-_an>A~9$XG-#606+mqS;RseIL*QZQ2)@`)){G9< z>I`o=XYHNf1<;@CfO`6O-Zr+vnnue=&UkiXMfMTx9+1Js3MBd>u9F27gM~@~Hh9C{vrf$UG$GdMH z8iD{)7QNe`Srvo##G`<)GC{ZY`qgaK&fwZ<2nO|#1nQt}F6^Rh{z6XabJtLE0B>2X|0>I;V#>TQYr7N12r=4K4!aM=~3E2 z)gz?VoeTHo4U}nqJU+uU9`l#}C<8KKO%tv(aewC`0;1NzXRdOKqP}%9b-_p4iJ|kt zjld&PrfEW{#@hK1NyliNcI6TGya*y}?^Er>$53p0jo%IUR3dPfwcCDa7_arq^;ss; zR>==K)?vTdo|dc?4Iua|rE+q31rIA|j{<&4sHLA;C%x(2@?~JiE8=x;;GZ}8QUh{q zZo8k~e>Y5zOq$jF#YXzCCaD#@Nn^rvt*V?&7DZzr=ZW3kdHS_B^5KL?gp{U{f!LOA zIO8L~YpS!Zf&NWo1tZ;y%qiX?hJGWXLTTb3X<{0$GaH*QAb#YIb5%E1iqqT)K3a=T zXRnKped0J$6R@)S@Phn-4!a^G4|9t&O8scwbpcduz62TajES_U_C`h<8|rZxLcOz7 zE*78-DBv*Fb9wU>80!ij8odcCTjmWBuNnYJt@X#wL*Ygy_ryVA5%82m4nO%!O@}#q zaA8OG5%kmJHGF(>H>UMRMQr$|9P(#qm*VpTk&%LmpWkN4lkq014|z}C&&H|Rmmd}9 zy$=&>J;cjD<=k+``-_dnk2gw9i0#?zHtx7tm+D7cPg9rfS6(gZvBM-Zu{2BGqSW8| zBeT!hz+x&6f>w~MEF%8&=J9i^Tm`@sSDia%vxA*S z;eX^ooc}$*YUtG8>p%vSe|l|{F-F1+YKs%Rpx39=ys94dk<4PT0EFG}x@UuToY;I+ zUGwjDO8K`>%x6ogZw1w{dJIrE&Rcvokk_4Iua%|SE8XsSAvft zh94vlxnCJbM(MRFF|qjdxBp13gcy4kbIyY7udrlZ{Rzq_-TwA+|Jj3AXWiJzwYwGj zHOPSW2XKy+yGBGvd8(I;7+1CVo5krF#}NfVi#s(a^;W9jsm`)QAJ)rF}$=9>HKr5N`Jy>xy>eX5^fJ2m-t|Xdwbv6Co**~^M`jD zy_rJ&oOLB;Fy@A1^ltFDjfnlBrtFB01V@nQN8h$D)A2UKbc@Li4vQj>p{y;a9$jI) z@j7$D9EI#jGhnk{#ooUixuMHnD8`N+2^UGQ_{dJ`_`BF8e=>$?RsuTLBJEd4ZAzk8 z`QxMSlC_kGAJv148>QsQ6<)(aCrM#-OObf_hB6KxfDH>(pxws*`{+7(DC+x?D}~A3 z|8(kdGReE3+1RO2cTkJ9{X#l7vcul?OBg(|S1qk-xO~6X4EX+=$T4&O)%gf4akZ!3 zTr&`MEvCjV(&K=7PN48?uWwhN?sa0dGiJ{Z6#wwZQi27->~Wn5y#P|SNRgq zzMCW>Eb=1mN}r#2G&-#chdFc=RQnt{Pm*Hhw$4>#Ef*v4D4162&ZtxD$!^W9Gd}H_ z(*6NyZ}BCjS1^Q9dW6Xz*8x25HbRDd_dY{dG~Zzmvze~~6BJIcm^{euA@Swox zYUFNY%J-2gX(2R#unh{P zV+GZ)AX#BP#5LL1Qu|7qEh9g7CxIrlF@A_r8KddBk`k8w(7lM3ng73wE3;a4?v$-R zH&pa4?#@&9_Bt2mE#g7|IzIH=q(u{7EC?QjD6JduNa;V|w&3eaB=q?81jRrzL_^=`2EN)Z zzCK4~!}lUP$Q^y!&62k9#5?VNCAo~I_hbEFCi@@4S*5M3zkcw-LN&M|4*auX*Lvl+ zmsGL3%{K4%CYpCh`Uo}kGqvs+_K^YMAa;1CX(j&88e|Xn8{p^a+S=T*z7R!)_Zh3$ zpe277cQ#n`vzL{6Dmql2UlU7MGPscn|cJP-{Vk-2$)GEW&F)+VlIz%hj=b)E;@q@9}2h z)}O+-9YYnceA6rOA^S2^F$d9OV@%doRuDlWphS2qZx(-=%f`=a2%vHZ$)%*j#65wc zz6c|)mT0Xjjd(hYVi?IiS7Xn_4U&Vf?}4RP&F$WJI~6itrupBJrAN92sHbNE7$xsd zq97RIK{?bZ{;ZZ3J!9&i=P$91{3F4~&6C6!cT+X?ek!|%`nQC2%lH4g;zfSzA=5$~ zs|^-7AVmuCa}yv#jsuh&2B+7I_gMK2e_w_9eZ4EdC}?zGKiQ@PWCuxRYkvvXmpf+O z7o$z=>n;E;E3l01<&U)Y%rBWW=Ti&(=(6HxBv!i^S?C&_r;@`J7=hHC-MJGnydwF+ ze}Vg#Rk>__KI;2WL2c!WbE;u)aZq8!OX^scHQSKzrI2Q*U_g?K(9w3R=SWVe9^6YsF5&{rwK#Orh)h-}hZ9jCmTzOH_)(tc;- zZ;}C>A8FAklPRCKo6L-b&$Xl$0HB1`24UE_Y)%q4(j)4S>J9TS^D=vuN=S=uG}Pf< zr@>>WL7hq`%i@10) zMPJS!i!kJaj)T_kMqaCp1t(qI&^6rW9v64liup8}d-1@EuxLp}d;eNkOQ1f7YO4}F|;?}yJ($fer<@LJVGWM5a4w8Gw@Z9%h9}i~i z)wBo^=jUm%UyO9K56DMshANqP5hj?ChKERC~n1NHFT`(jNX>us)@u+3JOq=Mq; zCLBFy%KtWjrcyZq*&WnOzwFz5%+HJ*oBT!%NLyS$HRrPYy<5x)m zA8B}`Y^$hqw@@&xo5$W?IHK!r@Dd4}?T1}1pY>}aa@ExbG0OmY3l~ZLm2n)a0zcOV zDM0{yGP|KrweyzYhHO?EQswsYCo_xB2F^Nvj6)Dmghm&G; zv!0A3&?z})!#pu!MWQSf#7QVHugYjYsz=4Ws!tc&P?9yHdZq1_`cCYxAU#$){l7Z$ z51?6g#Y<0ZTyJlHTNiOd$;ARV(0CtRWu_h;RWzW*lV`BT!OlK_2|yD6X6fc)PLZ zn_Ug)Ae;ROw3lyZ)lLsQ&N<3Vx$ed!y`e0+Vz8x_g-=pNq5%-seAsRQVETP?Q#b)I zziXf);SK{0%uWVr2ansIq4wONXJ={I*~bw;RPm|GG2o0UMjaAs7H@xW&eK z>ea&6n(z*NC)whgF66t2AC4@quNdT&Bl-A z7~bA*zIEr11u;)zNARamdbe4c3s`ea?`Dq#NL+2RO;&Qt15VNa*U=fr?I*JgTc1FP ztPr5OOpTm1K&rcR-pxuU#@#ngWVTq)O+doFtv)b5{Es1c1KCXfIDba^U4$_i37jmL zw%BV*m=b%99`uiK33PvCG=b)5cc$y(tP|Ptl$~e2tgdFiBI6*u5}%7DLf&La zxl8H#j6DMN`Lo&6vOqNG-*w_8u=>6ku{1b5+!cQSGzP}2&!#;N1Z0EGup|rLEEyEb z3K6Gf(qAx-gjY{^31DSWs$RR|@OdsG9c!MXeJ&~(-IlX_o_P^O_R9>Brm9uD!ORyP zZiOGEfn>Ly4)uAgr&6XFc}UK_m$51m4G`zwn)=|pU7uI~Q8IM1A-E}GEl0M&rKJkb z_D}hRJ^0}KkF@pDffIZpSdlMR>slsaq~`PfXr1*ZQ1cg-0K6zrT{nW)i}l(I+^fEP zzS}#mCixO5Z*Vi!=~T(B`))mB?mYHWRTY~A>T%{usAepc40ra1y6>VL)L$7Za?zUN@F zvgK=Sa`xV=4q%VJUncP(@%Bd?Fb^*8k{JNlfb@MgX7t2S2*f(%-sX% za0n9TmHaeCzH7u5c9Um)ket#sVJ^gIVW0eQfWv4(uk%ip9Kkw*sAxXik;EAND=_?U z!{Jwf;OVP)#o=|btY7&Y4^2@oS>xBPMHP+DG#WmXr>RNjWb!#^mtTAL{@-FTdei7@k!8XE zftFE8jql!UiG(ArPZ=Kf4#3Syes@HZ@OR>R=J%g*&6R^p^Q1%xeRJY#`YGbw_S_RX zhUhzQn%6vcYihNw_`L=03X2oA%N<14oMG!<@rxPyA#JEU#*b$-4Y>B0Z+~0^9`nx) zA8V=KOA+yOu7Fxepct(23Sriw5qVTEKx4<{8Amt&@=saFy5PKgR<(4_py&5xOViOO z<;<#=Mb9zfHQx{0V`!_MAB_5r9^{%kvGvxjnharo+Flqepg}`mpYpwBJya(~JzMDI zp#rp1kwJ4$o9G;Kg(!PF8|KWKrIpz`w3osfE`1uwo24XrZ zZ8b1@k0O&kui$y8fd%W~=xuaERt6}EtljPFEDM53HLJb>DwLJ6B0WOmSBB|$F&#NFo7Fe^aBo5My zAQCA;X4hn8;^QQKGGdXSO>&9nz>{AsZM^6TE6Jaa?whG98S51v1Akap*j*63k5h#k zV|O=iTHd+->}RHpb&q#4?%r1p8R}HO3)4N_fvSzzdHM!5do6M@cX)VXZRhlN&*h9pelM@F#AbxlsI}8?P(eV<#mQWOK*$ z%H+x2H%i90l!FZzo`q+(-`6u-;CH(svzn1y``;zQ{nGxg$%Z(u(5$hbK&73X*U>{+BE?#Z;8h@(WcogmT)!^G!;(yogRW$1>FrT;m+!U~i zeLVI~V)EBSPI3-<%;9-^&ZO*E*5nsiT{nTTcUpO$#6;!9o|E-kjP4Q*OH@34EV59M zELyqBX%N~-7VJkQDmr3uHRQ&lx1l44izn~8uEE*-)0Yl>(w5Hh(j;l+o8FMfe~fOJ z?xV(=U~4+Qg{C4H&Viy{HP*}bFUQh6(9&`nBBjp{o^4uT!^6Xyxq?4LpK>_NsKZRu zz-GYt8nYKxR>_)h@2M8zBd@b|jpE^0i~&KiZ^fzzAO)Uy3o$kb(I5%9`6-vqMg);K zIIPBCd%jbV?qzFJBsU$4#k`^c!|?3i9JVfbg%Yt)Vy2Ep-QlHTUj{fAx;|^(hTe`* z@JJ|h`KoWE>QrsmWl@5{pfI0EXSEE|K730mRgU{?6t(ZM;PkNA%+x03GAC4f2VT#c4?{lG@gC4hpbX+3yf&Fst{5-v#5qY+Q&| zrE0axVsEXIjqPfoZH-*5FB4M^gYViYyvSHeUHWkO_RRU&M(|v4%N7=U$B*A|Y8uw} zY-N37CFe)5HlXK^&lPpCC7}KEd;LY<>U_{zU!>NQBH}mfhJ?kNiK~?LHmfUUva^m4 zlCL$X1qB854>*sBuk{^^WlBo<6b{v4eTz0PIL&0vTx2$dq))ATq)2gRdSEIQkUfjt zx_D7pR$o&ywo1#?tA&2L7n_|q>xO^I5dRdGuYp|Y7&o$@>A^osTRmXGErzm~st6sb zt=oSd7xJ}18S@5{h64l+=4vbP;4q1~!XvPLDlc=76oZ??+w`Wf4mGBXXne!}>0?+A z>)17B6VhTAa;3*F@_h%(lAfaRAcpv0rV@A)8J@h!0M#M0ze1jP?{KK@kHf`i< zK;gn;stEj^gN-A?=FQIi1J7K_b0@!2++K=!9^em2`yr;zY8T#nHp;;Uu+15{(%rF7 zZ)gZ3dapl1{h+7(#FT?oY#sdAq6IBonqEO^x2imbQI_xjeRQWFwAQTe?F$-LAD@TT z;%=Hu_UXI~%YEsp5jr{S05QSwXm)NxF%PZp@LSbPwJW{hbb7(~z!7&i#=~J!uM6E3 zIQWNMrKiq}da^J6k`=2;JyLTj`u5?6*>^vygYmyd8fN=-dKANS0(HlA;k=F3Vc64e z0q`$ahGibUGjDrWbG6(yn3@T_igHwJ=Mu;|dNiGqX$lv;L40Vy} z>1w3YY5K4yEplji7dzqU9u9p~emdkL6~ii2rt7icqI24$ZyA8adJO2Gq;1DBZt_lI zt3y1MPwnhdkprWnvnt`J<}b3DRsdqbPUto}6h0mh_mrKUvV? zOqNdtljxMTk9fGCnTT#u;F5fGXjZ@X*^LB}Gr>g^T9 zS?b1pgX(buVY1X!5f|;ffC3_)At)5eLN^p$oi?c2XKWUMnjS8^zEga}^4W;(+@(& z?+0zqWBj_wg22quLctTzB@i`bxOUuwQW^rDVmO*U!ve@j3mH^6zDIdt@ z&%Ds~Y>`S4r4RJl6v98rZAZN;8r5)@jf+0+rTwx5+sw$TTs8ijQ@}6c`?_3cLQ)GY z>appcviy+GqSL&c%4UMH z9*&-)pwQe+Tk#OZp68|kA&6Y+`a4M#3=e${%N11g47w(&cetKeaK65A{rbB~U00d44zsL*GF^{9=dT0`IP$d*o^VL`>DDS+K`?mXb zhI=1l;rA_u4M%;QKZn(t*CmT=`>^+I6G{X{uL`w!ZIdK_-gC%B#w%(m{{F5*^SLOv zn%^o+OZ%Z}j`pzMHnO(NUTCC0V?}oV!!2dj1gedx^R5@TiL2V;2+{dGHzl6Ypd6Iq z;-j`(3X~}4OHoZLDekO;weE7>^qXEf&{x#0ByI+b`bWEu17~UD4bj#E77EyrVj8*} zjJ>h}dna#vbF|_IzdM8dMJxIZ4)p6a1PThoV1^kneLsIx@@L*%y z@|f_Xq=UiJd~LJi+a89o@J=yrC;VW4|M#}cERHAf5(PD%Ka0`jA|3po_sZF=W^AGC z%h+~TP_Xv-09Lu&(lQw-Br1BheyR6}<#<0ulCR@!12^tLJa5t>f}DjlEbU)qBW5uV zX*5S2`P)4RG`+89rpf;e%WpMLKO=tbP%7qk6pZ|rcXSgA0oos{mIkYwF#TW3`zKDd~J);b!nxAwuH!^QVtD^s_Pi*~NMn$hT#2DQ(2d zU+hj(1m?2Gr>T#4RVl&xA^GAy&-3!WP3li(Im6shK7i)Z^_Zg^Z)XcXAm3JzDHB7Qt^dv@0+IXcSm8;*y4r{ zNBfJQld1bue4RH8QgN>bPyZJLA0h4HYFPGO^SX;JTBl*ow*~q_ zNOSOjOZoWN3Ft|>HMIlj`p12jdKMnU9vt7-!ZbDpE01hwaQvU}APUAGkde~ERp+|r zJ~)ba`W#K<{%P?RnJ0>X`-XYa&7&u|i|_of6cAJ%7UBA3n%AcOH0S7=W4 zt{5M}biAkoI4_?iBY>rI#Xf!dAuin3(#xxj20_v)uT7?1cpP&1Fr@5x0OrY!1em_c=)M7?=ayrveJ8*YD zo__|!_z2zKv>c>6k@mx)Eif`aXm;)SSm|#hd!6;pdO@hT6fM#qO7)(wasgkkbfp*j zyAT2GB`@bBoImvj!JoMm=;x!f#lO`1bZmq`d6xk_w@@mFAr?I8>~N*uRe5zo(=6(_M@pV{Lj6GE(o za~h2(2W%GN8vS}GN;WU78?nspLW(7(9=`kaY_>1sMC6V@xd~3s!fX4LK-6%fkh(~O zGuD;1Oq!-YctPr5q(qlM%_o*Eai2p zl!XE=LO@0O)-~fJ+1xwt7uKa->hd^LzTTdx5e6`OYG*C_>9MLN%R43d@eP|(Hv%L1 z3n_c6X5&C*d*U#be)43`yc`nVgU*4gF0kTT;F<6)(s5 zYAVO9itqXCpYlw(Fv~LC4E5VyabhyHR=9%OhvQ@ag;*!Td`SjX7EOlX?=CgV-qaT( z-@9kZt(t2?>=+)SQIR=H6U){?zMN|->YiCEUf%6pxTfXk%A?;br(A{bX1XdDSa8hzT4mz*oTC)NQU7a z7iNK%vsy7B$m3JKUz@zW21prIBYJBO(8;|Rw@NmCu+KR!l==zg%9Sf^!nUhHx+8_` z>WRx^pBku?=3X%mq~|LC60U#Y%OXezi>?>UXJeO9JTBV zC>nKC6x6mygjkms>vImK?`UQxvpv1gA0ox^Xd5wpJT)vu+A6bYvi~?^->^V$Cr>uK zfd!iX&e4EV`C(ch6EI3})38y2RT-P`!nH{aO6jbuti1=Vl1^4~4QZgT{nVo$dwC8V zF%aCyFhT+tYt^}W{_*hy2@)|xQdwUjUY5N!oaKZ3p+eF|ptPDLpiA zL$;>vfYq<0e6QRMW(rM0;#5CJ-TZjFd#IqinCWY(C9uYBt12ogUW%6)DIEEEa#(|X%m;g~0HI%6f?)g*wK}Pm++IKkP5u$4Rgu zdrR?tVJ@h!)Si;x4bJM8cjHhEIJFm#lmLI|p0k8c z_7##zH)Ac$Dk-0zEn=w@KVH>A3x$=F*&N@mt430cD*t;efLTIxo`#>`_3V*nqBu6H z3aA;@Mt|KY#14M5jVc1IsIJ_)mGX1SS06_l$6FyuC^9sDM=nJ7uEQ&d2_8o?6TAMF z6V}^jo~y86qztfYcpPxoFa!z~kN*5wR^N%)kY|`yv>;S0)m^4KYtpDt%@F;iN^b5+ zZc*S$OaDy7GR~~VePHze6`o)|FnHDu0+}|4-cwJgqN^l>Wj8z8u3jU9uf}jH8{tkS zjF|v!5`WkE=G4`zS0ii-31O*C5v7Hym9$7lG1TVs(U(ug-ZIlmEzifDKg7|7h!7#K zy)Z!)nSL0j7c}@3Sz6WTn@~p4gMlcu=2Ih?aohpXSijkE?P0Gr=?j*bw(^S}a>`<0 zEC~S(9Y&kPq{u4f;oo(+)Z%YM43Pf{3DKf%#s;dbIcF!+F)qiSC$37^IUE!+|GUXe zqsd@wre5AjO(kqVy#jhrU3nwUwIQf=gt}AWGM85?&X>x}{Gl8t;F>x}i}0G{+u4+| z$=MN}f;?SjsM|DT8@5bHN-8Qv{$Z$3^{s)+*XBS>gp_U@9IP>z{S>siV=XEteJN*# zqp4uBRLsiu*NiVYIpiijqU}R`4Qt*i5HtMMlElA+2_E5&LwXQmOnHB;)bVRsnd}_@ zI=*V_psr(=tN*3M4g(6G$%8+lO(hxmztL6mVoi=wWU&AHoSZVptglaD{=gcMYHOJX z39`_raTSji_cufWymMH3U!-lJcaeUAhb;+0W%7FXr_r6;LXHRM&Lf%_Zpy6PnjcX6N3ib@j<)#Ic^77Dj?JSOXP4m84WBmAw{ zo0u$sk7-WVvjnbRaxdUhzDtebb#C1$`^<~Te=BWr-~@vRuc)$@-3wT#AyMMf@v3a= znj={Zvjv=}Qg`rwNo~tCOs|m~$|jw3aZ(EEM~*XPqVV$bV`(pi9A0ZQUJnT&<3!=L zJ5TPk2~*gb)u@Vir=4gV1!l~~p<)>i{)*ng(Zahb0%Xg)mv?)%ncQ>z@zPH!Fy#IS zM>ruAQwliwa?HI6AP8q_IwfN6#`gC=ceL(Py%{U2yU4Pq1&0o3cXyvq;l_nA08+aL zwF1Sex53OdF_kD5SovxA38yzlX4)ew^W%s;8QegBBS6nVGM#a_JC=U#W1RIZCH6iC zz5Nj%iyX|yX;C)ZAiTo>u!riDaVl7xa?M7wjlI}i1W@YK=E)~rU4OQ81<9eQFO{QH zI1puZ0c2>i>Hv-dct&P^FarJhnEn2MqDaq}rjB-p4*A3-Z#A$tZ9koIq()Q&O|jf= zmA@GNINfqc^475=%;y{^$ZeX}hflxNUXs0B<@+uRpkSch3AgmwBqE?a+7kjM6;5&W zPfP6d+$k>C=?jR4ql8;D^PcOMRy_H_;OFxe^nr^hl;PJGdJOwDa7TyrQj8ylgB#VS zI;F~jz3I8$13O0B76#MhG2larR~(P5cMTOdqyB4MX zGx}RNX)xw#{$Jg5Z?}p|mdEqu08ZPH!pykJqLQ~ zR$98GT4Y6ii5^gBFYo5dr=ATt#?+dUua3F2Ec>0WkN`J>oGGgCD!<>HZBDCjx#Q#5 z%gxQ;*kj@9uCW%=a&y1nfrR+maPuCPS`5UjU^6F8zhBBo`(^^HULN-1m>l;6op0-;(Iu25z4<$R@fLZ{dy39;|49D! zOBb*26s5l9db-rEQ-kv{Aqd@WpoJ}$0BvSUD)dLC-$mp9#8IFi(1`nl9yu`^TgTL< zDf~Isl+D&adU4FQ$lbrKT=u74G%j1{yN$%eGO=E}*5i79O4o>mRiI~n&T%U{@V4@| z+>>n<_}^_I{WGy?TL)N(QTHvwZS6YVenfI=GR>cw?HoLQ=t<|!G$L{Shk^)++lN0J zr6YM}9~zv!Runx{h-kFLx|Z$rgF?U5&3{KN3hbKYv9!W#H95AyVw4u`-}Y1t6X;qV z2gx^f|Gnh+DkfkzrK~^XW6+E$zmLiWuma_5k6c_Y)fPNPU{^cvulf=wMX?&K#Z~KN zqSo~u)bkXnaWDS6UGS>l{`CI({M<+9zK$xbP|9MMPUWH&f-Gu)ma+WgavgZufK0!0 zjy$YLbJ1{jK0>A6yVs{Fw6A|EADeG@8HTvD{uKJGZ%QDCx+mfo-#^-;2#`2#C=$Em zNrxyrDBI}EZ{jgO1CNipOjUMnlGLSfjbv_w?ti%4$P5!c!XMb2rUC6duxtsX@rreZ zs@#R+-Af-h9vawx@8X2v=8oh_AOc|MJb=IP&+Kd>2Pd!$#Nz;I9F_;h=j?a-i}8I)5|mez~*JOH;>Tr2g@ zvyW$T59f;dxpdZ}Ij8hoDo@kmt&;xh6yCVO&i9cAEmGQV>zvN9zV=PEmOG5((Ngb~ zXB#dw4aZ6ery2(^9Xi(5U%w}bPuJ?b>pSQf_df6#FhOX&;f;;UXL*l`v>*+^>A&WC z8gvzxoBMwA_;ypjw9R5wOh1w|e?7@VX;!iXqUob^T7-dE_^6YXR-_;Pq^PJ!4>Pjf z{e0Y&GjuRE9^FHN>Zt)Xx^@)7a~G*#@{YV zQkC~A4FaFH0fnzT`sZ@~WUX6bS_?1E7cJ1%{}rasDq*6YTloLPdvM#&uuMx|cZE0B z7an4(NA;GR%of5Adbh>Cp#S{+^A31yXs!smep>jS3(n}xzJWxw>3ttuU3tJ{&+ua__ljaQAp^%34Lv`L-DF&ZXvM7)+H`2F1EIo8^-k3@AkAWoOpL7bd*gf zS=YYlR(Z7>2jNqNc1=qgQGXMY(=$r~bZ` zQ#@CO_|l98Cy%DMypgi*fREtU$9NWj&yDGRNPCf%VM%ASLmUAy$v;&Jfn z;>Y)EP_X*(trakagqQQ60|M|tx64rXXadTT-H3i0_U3<*@=g|`F+IwWsYjO);eFzG zl;I%v6zEhs+v)4^sDs|ki0RfR6r+`PlU%@xA-i-8;W=KL#8X-p8WJ&DCsh%NFQ+B@ z^Z@0`Lx~Y;BX8A=4?zG$rCx(rxNJ^S26u4M-(|&{o{ydqzhVL}`34!Gtp*YZK1JY+ zmQ7027X=3{@U#D3Pm5W6h$fh(t|{**iZJ+6XY?11^!3-Gi;shYhpP2eae;t2*;X{L z<)?t}Hb4LOn=+P_ZG!wc=F4~FF-L3s)hBm*yEC8r2Zl~H`N8A7Z*S?Lz_B4;GU}jQ zbR*qngZj5r;&@|6>hjU#1CH>Qs*_fw?JzmwM zypoj>3QK+S@0y?E+y{IBSw)?2EAX0Zj5{tWMh9Voe%GSF>y_!% z!S{yuJGjiA=MC0|P8tc^;vQFfeV8=70x$kP=^AsKn8$l5@9;=lgb87k_EV33ZA=Q+ z)x<#La}(Fokc*Rh!Z|A0}Buw_lp-Vp~>dFEhRed8d$ymOMWT4!al3&bN(AI-Z>P-`tZL)0{-SM zj8^l_%s%tx>4sNcOg-In2+4GjXBm>b<}V8VDFLzxQ9e~lR`T(Mg7)GcX(NT|j)!)i znacDfu{ai#yvO@@W`{lc{@Fn5{iI!MX4iA0*lNq}X>k)g#@p2FA9v!Nnh7@X_Q$~^ z+2i+JbpEV#TTrIRB275I|NG2-nTs_&M?6MfIOtVZJ-Byorj7&}b9blvpAmp}YAV_E zoXK>V2NnO?+e;Bv^fM?a>NuBU#*Aa{JyszVY|>p{*N0r zm8=M0d!14)PapWnpo@y0j0`WGmwhvP&vI#P;?1P16C=IEX4C8A8K5xqwMD0g7>XV) zRgQnm>r1ZXpIyQH!!v=n=E;bt{pYH9sSUUC_p$#hH~(}vqcP7~Ye|Ws0(HE?Q?gvh zrR>MM2o`MW*J`2k?u#J4Lx4^h4=)3@81QNTGX?xv;Wv!Gf>)-jY@$P2)Vnm9-iy?I z{!d2W`I6fCX9L5J2e#Inr{<%h-@PzV2+4~fyYugR^Ou6(H0DWdDVbRhw{J~uZkBOW z8UD8*yi>>uo>FcR&=J)2oqcF#o0|ORPfpsiWjX$RbZh+yDRo literal 0 HcmV?d00001 diff --git a/src/assets/user-profile-empty-state-wave.svg b/src/assets/user-profile-empty-state-wave.svg new file mode 100644 index 00000000..afdd9849 --- /dev/null +++ b/src/assets/user-profile-empty-state-wave.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/css/unison-share.css b/src/css/unison-share.css new file mode 100644 index 00000000..56a9ce8a --- /dev/null +++ b/src/css/unison-share.css @@ -0,0 +1,18 @@ +@import "./unison-share/app.css"; +@import "./unison-share/info-modal.css"; +@import "./unison-share/welcome-tour-modal.css"; +@import "./unison-share/banner.css"; +@import "./unison-share/help-modal.css"; +@import "./unison-share/report-bug-modal.css"; +@import "./unison-share/download-modal.css"; +@import "./unison-share/setup-instructions.css"; +@import "./unison-share/use-project-modal.css"; +@import "./unison-share/publish-project-release-modal.css"; +@import "./unison-share/project-contribution-form-modal.css"; +@import "./unison-share/project-ticket-form-modal.css"; +@import "./unison-share/search-branch-sheet.css"; +@import "./unison-share/readme-card.css"; +@import "./unison-share/page.css"; +@import "./unison-share/project/project-ref.css"; +@import "./unison-share/project/project-listing.css"; +@import "./unison-share/timeline.css"; diff --git a/src/css/unison-share/app.css b/src/css/unison-share/app.css new file mode 100644 index 00000000..239d0bff --- /dev/null +++ b/src/css/unison-share/app.css @@ -0,0 +1,238 @@ +#app { + display: grid; + height: 100vh; + grid-template-rows: auto auto auto 1fr; + grid-template-columns: 1fr; + grid-template-areas: + "announcement" + "app-header" + "page-header" + "page-layout"; + + --c-height_announcement: 2rem; /* its 0 when there's no banner visible, 2rem otherwise */ +} + +/* TODO: move this be to the modal module in ui-core? */ +body:has(#modal-overlay) { + overflow: clip; +} + +#announcement { + --c-color_announcement_background: var(--color-gray-darken-30); + --c-color_announcement_text: var(--color-gray-lighten-100); + --u-color_interactive: var(--color-blue-3); + + display: flex; + grid-area: announcement; + font-size: var(--font-size-medium); + height: var(--c-height_announcement); + background: var(--c-color_announcement_background); + color: var(--c-color_announcement_text); + align-items: center; + justify-content: center; + padding: 0.5rem; + + & .announcement_content { + display: flex; + flex-direction: row; + align-items: center; + line-height: 1; + gap: 0.25rem; + } +} + +/* -- App Error ------------------------------------------------------------ */ + +.app-error { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + padding-top: 8rem; + font-weight: 600; + color: var(--u-color_text); +} + +.app-error .icon { + font-size: 4rem; + color: var(--u-color_critical_icon); +} + +/* -- App header ----------------------------------------------------------- */ + +#app-header .account-menu-trigger { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + height: 2rem; + padding-left: 0.25rem; + padding-right: 0.375rem; + border-radius: 1rem; + transition: all 0.2s; +} + +#app-header .account-menu .action-menu .action-menu_sheet { + width: 11.5rem; +} + +#app-header .create-account-menu { + width: 16rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +#app-header .create-account-menu .button { + width: 100%; + --color-button-emphasized-text: var(--color-gray-darken-30); + --color-button-emphasized-icon: var(--color-gray-lighten-20); + --color-button-emphasized-bg: var(--color-gray-lighten-60); + --color-button-emphasized-hover-text: var(--color-gray-darken-30); + --color-button-emphasized-hover-icon: var(--color-gray-lighten-20); + --color-button-emphasized-hover-bg: var(--color-gray-lighten-30); +} + +#app-header .create-account-menu .terms-of-service { + font-size: var(--font-size-small); + color: var(--u-color_text_subdued); +} + +#app-header .create-account-menu .terms-of-service a { + color: var(--color-blue-3); +} + +#app-header .create-account-menu .terms-of-service a:hover { + color: var(--color-blue-4); + text-decoration: underline; +} + +#app-header .sign-in-nav { + display: flex; + gap: 0.5rem; +} + +#app-header .sign-in-nav_mobile { + display: none; +} + +#app-header .nav-item .tooltip { + line-height: 1.4; + margin-top: -0.5rem; + margin-right: 0; + margin-left: 4.1rem; +} + +#app-header .nav-item .tooltip .tooltip-bubble { + white-space: nowrap; +} + +#app-header .action-menu > .nudge .nudge_circle { + box-shadow: 0 0 0 4px var(--color-app-header-bg); +} + +#app-header .account-menu-trigger:hover, +#app-header .account-menu-trigger.account-menu_is-open { + background: var(--u-color_c_navigation-item_selected); +} + +#app-header .account-menu-trigger:hover .icon, +#app-header .account-menu-trigger.account-menu_is-open .icon { + color: var(--u-color_c_text-on-navigation-item_selected); +} + +#app-header .account-menu .icon { + color: var(--u-color_icon_subdued); +} + +#app-header .tooltip-bubble { + margin-right: -3.9rem; + margin-top: 1rem; +} + +/* -- App Footer ----------------------------------------------------------- */ + +#app-footer { + grid-area: app-footer; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 0.5rem; + color: var(--u-color_text_very-subdued); + font-size: var(--font-size-small); +} + +#app-footer a { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 0.25rem; + color: var(--u-color_text); + font-size: var(--font-size-small); +} + +#app-footer a:hover { + color: var(--u-color_interactive); +} + +#app-footer .icon { + color: var(--u-color_icon); + font-size: var(--font-size-small); + line-height: 1; +} + +@media only screen and (--u-viewport_max-lg) { + #announcement { + font-size: var(--font-size-small); + } +} + +@media only screen and (--u-viewport_max-md) { + #app { + width: 100vw; + --app-header-height: 7rem; + --c-height_announcement: 0rem; + } + + #announcement { + display: none; + } + + #app-header { + padding: 1rem 0.25rem 0 0.5rem; + flex-direction: column; + gap: 1rem; + align-items: flex-start; + position: relative; + } + + #app-header .app-title { + margin-right: 0.25rem; + margin-left: 0.5rem; + } + + #app-header .left-side { + display: none; + } + + #app-header .navigation { + gap: 0; + margin-right: 1rem; + } + + #app-header .right-side { + position: absolute; + right: 1rem; + top: 1rem; + } + + #app-header .sign-in-nav { + display: none; + } + + #app-header .sign-in-nav_mobile { + display: flex; + } +} diff --git a/src/css/unison-share/banner.css b/src/css/unison-share/banner.css new file mode 100644 index 00000000..c6957c09 --- /dev/null +++ b/src/css/unison-share/banner.css @@ -0,0 +1,48 @@ +.banner { + display: inline-flex; + height: 1.5rem; + border-radius: calc(1.5rem / 2); + border: 1px solid var(--banner-border); + background: var(--banner-bg); + color: var(--banner-fg); + padding: 0 0.75rem; + line-height: 1; + align-items: center; + font-size: var(--font-size-small); +} + +.banner:hover { + text-decoration: none; + transform: translate(0, 0.1rem); +} + +/* Promotions */ + +.banner .banner-cta { + color: var(--banner-fg-em); + border-left: 1px solid var(--banner-border); + display: inline-flex; + height: 1.5rem; + align-items: center; + font-weight: bold; + margin-left: 0.75rem; + padding-left: 0.75rem; +} + +.banner.article { + --banner-fg: var(--color-green-4); + --banner-bg: var(--color-gray-base); + --banner-fg-em: var(--color-green-3); + --banner-border: var(--color-gray-lighten-20); + text-shadow: 0 1px rgba(0, 0, 0, 0.25); + + color: var(--banner-fg); +} + +.banner.hacktoberfest { + --banner-fg: var(--color-orange-3); + --banner-bg: rgba(255, 136, 0, 0.2); /* color-orange-1 20% */ + --banner-fg-em: var(--color-orange-5); + --banner-border: var(--color-orange-1); + text-shadow: 0 1px rgba(0, 0, 0, 0.25); +} diff --git a/src/css/unison-share/download-modal.css b/src/css/unison-share/download-modal.css new file mode 100644 index 00000000..c304b3db --- /dev/null +++ b/src/css/unison-share/download-modal.css @@ -0,0 +1,18 @@ +#download-modal { + width: 38rem; +} + +#download-modal p { + margin-bottom: 1.5rem; +} + +#download-modal .hint { + margin-top: 0.5rem; + padding-left: calc(var(--border-radius-base) / 2); +} + +@media only screen and (--u-viewport_max-sm) { + #download-modal { + width: calc(100vw - 2rem); + } +} diff --git a/src/css/unison-share/help-modal.css b/src/css/unison-share/help-modal.css new file mode 100644 index 00000000..2b7e02ae --- /dev/null +++ b/src/css/unison-share/help-modal.css @@ -0,0 +1,36 @@ +#help-modal .shortcuts { + display: flex; + flex-direction: row; + gap: 2.625rem; +} + +#help-modal .shortcuts .shortcut-group { + width: 20rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +#help-modal .shortcuts h3 { + height: 1.5rem; + margin-bottom: 1rem; + display: flex; + align-items: center; +} + +#help-modal .shortcuts .row { + display: flex; + height: 1.5rem; + align-items: center; +} + +#help-modal .shortcuts .instructions { + display: flex; + flex-direction: row; + justify-self: flex-end; + margin-left: auto; +} + +#help-modal .subtle { + color: var(--color-modal-subtle-fg); +} diff --git a/src/css/unison-share/info-modal.css b/src/css/unison-share/info-modal.css new file mode 100644 index 00000000..c1c4affa --- /dev/null +++ b/src/css/unison-share/info-modal.css @@ -0,0 +1,31 @@ +#info-modal { + width: 30rem; +} + +#info-modal .info-modal-content { + display: flex; + flex-direction: column; +} + +#info-modal .status { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 1.5rem; +} + +#info-modal .status .status-banner { + margin: 0 1.5rem; +} + +#info-modal .status p { + margin-left: 2.25rem; + padding: 0 1.5rem; + font-size: 12px; +} + +#info-modal .button { + align-self: flex-end; + justify-self: flex-end; + margin-top: 1.5rem; +} diff --git a/src/css/unison-share/page.css b/src/css/unison-share/page.css new file mode 100644 index 00000000..dc28e0ce --- /dev/null +++ b/src/css/unison-share/page.css @@ -0,0 +1,20 @@ +@import "./page/catalog-page.css"; +@import "./page/user-profile-page.css"; +@import "./page/user-contributions-page.css"; +@import "./page/project-page.css"; +@import "./page/project-overview-page.css"; +@import "./page/project-branches-page.css"; +@import "./page/project-settings-page.css"; +@import "./page/project-release-page.css"; +@import "./page/project-releases-page.css"; +@import "./page/project-contribution-page.css"; +@import "./page/project-contribution-overview-page.css"; +@import "./page/project-contribution-changes-page.css"; +@import "./page/project-contributions-page.css"; +@import "./page/project-ticket-page.css"; +@import "./page/project-tickets-page.css"; +@import "./page/error-page.css"; +@import "./page/cloud-page.css"; +@import "./page/ucm-connected.css"; +@import "./page/code-page.css"; +@import "./page/accept-terms-page.css"; diff --git a/src/css/unison-share/page/accept-terms-page.css b/src/css/unison-share/page/accept-terms-page.css new file mode 100644 index 00000000..def15ad8 --- /dev/null +++ b/src/css/unison-share/page/accept-terms-page.css @@ -0,0 +1,16 @@ +.accept-terms-page { + padding-bottom: 4.5rem; + & .accept { + padding: 1.5rem; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 1rem; + position: absolute; + left: 0; + right: 0; + bottom: 0rem; + background: var(--color-gray-lighten-100); + border-top: 1px solid var(--u-color_border); + } +} diff --git a/src/css/unison-share/page/catalog-page.css b/src/css/unison-share/page/catalog-page.css new file mode 100644 index 00000000..ae6e5dc3 --- /dev/null +++ b/src/css/unison-share/page/catalog-page.css @@ -0,0 +1,384 @@ +.catalog-hero { + /* @color-todo @decorative */ + --color-catalog-search-field: var(--u-color_container); + --color-catalog-search-field-text: var(--u-color_text); + --color-catalog-hero-bg: var(--color-gray-darken-10); + --color-catalog-hero-bg-transparent: var(--color-gray-darken-10-transparent); + --color-catalog-hero-explore: var(--color-green-4); + --color-catalog-hero-discover: var(--color-blue-4); + --color-catalog-hero-share: var(--color-purple-4); + + display: flex; + flex: 1; + height: 100%; + background-image: linear-gradient( + to bottom, + var(--color-catalog-hero-bg), + var(--color-catalog-hero-bg-transparent) + ), + url("assets/circle-grid-color.svg"); + background-repeat: no-repeat; + background-position: center center; + background-size: cover; + justify-content: center; + align-items: center; +} + +.catalog-hero:after { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + content: " "; + background: var(--color-catalog-hero-bg); + opacity: 0.9; + z-index: var(--layer-beneath); +} + +.catalog-hero h1 { + position: relative; + z-index: var(--layer-base); + font-size: 2.375rem; + font-weight: normal; + text-align: center; + margin-bottom: 1.375rem; + text-wrap: balance; +} + +.catalog-hero h1 .explore { + color: var(--color-catalog-hero-explore); +} + +.catalog-hero h1 .discover { + color: var(--color-catalog-hero-discover); +} + +.catalog-hero h1 .share { + color: var(--color-catalog-hero-share); +} + +.catalog-hero .catalog-search { + width: 50rem; + background: var(--color-catalog-search-field); + color: var(--color-catalog-search-field-text); + border-radius: var(--border-radius-base); + position: absolute; + top: calc(var(--page-hero-height) - 1.75rem); + border: 2px solid var(--color-catalog-hero-bg); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); + display: flex; + flex-direction: column; + left: 50%; + transform: translateX(-50%); + z-index: var(--layer-base); + transition: all 0.2s; +} + +.catalog-hero .catalog-search:focus-within { + border-color: var(--u-color_focus-border); + box-shadow: 0 0 0 2px var(--u-color_focus-outline); +} + +.catalog-hero .catalog-search .search-field { + display: flex; + flex-direction: row; + align-items: center; + height: 3.5rem; + padding: 1rem 0 1rem 1rem; + border-radius: var(--border-radius-base); +} + +.catalog-hero .catalog-search .search-field:focus-within { + background: var(--color-gray-lighten-60); +} + +.catalog-hero .catalog-search .search-field .icon { + font-size: 1.5rem; + margin-top: -3px; + color: var(--u-color_icon_subdued); +} + +.catalog-hero .catalog-search .search-field input { + width: 100%; + height: calc(3.5rem - 4px); + margin-left: 0.75rem; + font-size: 1.125rem; + border-radius: var(--border-radius-base); + font-weight: bold; + background: transparent; +} + +.catalog-hero .catalog-search .search-field input::placeholder { + font-weight: normal; +} + +.catalog-hero .catalog-search .search-field input:focus { + outline: none; +} + +.catalog-hero .catalog-search .search-results { + background: var(-u-color_container); + border-top: 1px solid var(--color-gray-lighten-50); + border-radius: 0 0 var(--border-radius-base) var(--border-radius-base); + padding: 0.75rem; + overflow: auto; +} + +.catalog-hero .catalog-search .search-results table { + width: 100%; +} + +.catalog-hero .catalog-search .search-results .search-result td { + padding: 0.5rem 0.75rem; + height: 3rem; + font-size: 1rem; + cursor: pointer; +} + +.catalog-hero .catalog-search .search-results td:first-child { + border-radius: var(--border-radius-base) 0 0 var(--border-radius-base); +} + +.catalog-hero .catalog-search .search-results td:last-child { + border-radius: 0 var(--border-radius-base) var(--border-radius-base) 0; +} + +.catalog-hero .catalog-search .search-results .search-result td.match-name { + width: 20em; + text-overflow: ellipsis; + overflow: hidden; +} + +.catalog-hero .catalog-search .search-results .search-result td.category { + color: var(--u-color_text_very-subdued); + font-size: var(--font-size-small); + text-transform: uppercase; +} + +.catalog-hero .catalog-search .search-results .search-result .shortcut { + display: flex; + align-items: center; + justify-content: flex-end; +} + +.catalog-hero .catalog-search .search-results .search-result .key { + color: var(--color-modal-subtle-fg-em); + background: var(--color-modal-subtle-bg); +} + +.catalog-hero .catalog-search .search-results .search-result .key.active { + color: var(--color-modal-focus-subtle-fg); + background: var(--color-modal-focus-subtle-bg); +} + +.catalog-hero .catalog-search .search-results .search-result.focused { + background: var(--color-gray-lighten-55); +} + +.catalog-hero .catalog-search .search-results .search-result.focused .key { + color: var(--color-modal-focus-subtle-fg); + background: var(--color-modal-focus-subtle-bg); +} + +.catalog-hero .catalog-search .search-results .search-result:hover { + background: var(--color-gray-lighten-60); + text-decoration: none; +} + +.catalog-hero .search-results .project-match { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.catalog-hero .search-result .project-match .project-name-listing:hover { + background: none; +} + +.catalog-hero .search-results .project-match .project-match_summary { + font-size: var(--font-size-small); + max-width: 14rem; + margin-left: 2.25rem; + text-wrap: balance; +} + +.catalog-hero .catalog-search .search-results .search-result .user-match { + display: flex; + flex-direction: row; + align-items: center; + font-weight: bold; +} + +.catalog-hero .catalog-search .search-results .empty-state { + font-size: var(--font-size-base); + color: var(--u-color_text_very-subdued); + text-align: center; + padding: 1rem 0; +} + +.categories { + margin-top: 6.25rem; + columns: 3; + gap: 1.5rem; +} + +.categories .card { + margin-bottom: 2rem; + display: inline-flex; +} + +.categories .card .catalog_projects { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.categories .card .catalog_project { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.categories .card .catalog_project .catalog-project_summary { + margin-left: 2.25rem; + font-size: var(--font-size-medium); + text-wrap: balance; +} + +.catalog-page .catalog { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.catalog-page .get-listed-cta { + font-size: var(--font-size-medium); + text-align: center; +} + +.catalog-page #get-on-the-catalog-modal { + width: 34rem; +} + +.catalog-page #get-on-the-catalog-modal img { + margin: 1.5rem 3rem; + height: 248px; +} + +.catalog-page #get-on-the-catalog-modal p a { + margin: 0 0.25rem; +} + +.catalog-page #get-on-the-catalog-modal .modal-actions { + display: flex; + justify-content: flex-end; + margin-top: 2rem; +} + +.catalog-page .catalog.catalog_error { + margin-top: 4rem; + align-self: center; + justify-self: center; +} +.catalog-page .catalog.catalog_error p { + margin: 0; +} + +@media only screen and (--u-viewport_max-lg) { + .catalog-page { + --page-hero-height: 12rem; + } + + .catalog-hero .catalog-search { + width: 44rem; + } + + .catalog-hero h1 { + font-size: 1.9rem; + } + + .catelog { + align-items: center; + justify-content: center; + } + + .categories { + margin-top: 2rem; + columns: 2; + } + + .categories .card { + padding: 0 2rem; + } +} + +@media only screen and (--u-viewport_max-md) { + .catalog-page { + --page-hero-height: 8rem; + } + + .catalog-hero h1 { + font-size: 1.5rem; + padding: 0 1rem; + margin-bottom: 2rem; + } + + .catalog-hero .catalog-search { + width: 32rem; + } + + .categories { + padding: 0 4rem; + columns: 1; + } + + .catalog-hero .catalog-search .search-results .search-result td.category { + display: none; + } + + .catalog-hero + .catalog-search + .search-results + .search-result + .keyboard-shortcut { + display: none; + } + + .catalog-page .catalog.catalog_error { + margin-top: 0; + } +} + +@media only screen and (--u-viewport_max-sm) { + .catalog-page { + --page-hero-height: 10rem; + } + + .catalog-page .page.hero-layout .page-content { + padding: 0; + padding-top: 2rem; + } + + .catalog-hero h1 { + font-size: 1.25rem; + padding: 0 1rem; + } + + .categories { + padding: 0; + } + + .categories .card { + flex-basis: 100%; + width: auto; + } + + .catalog-hero .catalog-search { + width: calc(100% - 2rem); + } + + .catalog-page #get-on-the-catalog-modal { + width: calc(100vw - 2rem); + } +} diff --git a/src/css/unison-share/page/cloud-page.css b/src/css/unison-share/page/cloud-page.css new file mode 100644 index 00000000..b9d203b4 --- /dev/null +++ b/src/css/unison-share/page/cloud-page.css @@ -0,0 +1,22 @@ +.cloud-page { + text-align: center; +} + +.cloud-page .card { + background: var(--color-blue-2); + align-self: center; + color: var(--color-gray-lighten-100); + align-items: center; + width: 32rem; + margin-bottom: 2rem; +} + +.cloud-page h1 { + color: var(--color-gray-lighten-100); + font-size: 2rem; +} + +.cloud-page p { + text-align: center; + margin-bottom: 0; +} diff --git a/src/css/unison-share/page/code-page.css b/src/css/unison-share/page/code-page.css new file mode 100644 index 00000000..2a7112cd --- /dev/null +++ b/src/css/unison-share/page/code-page.css @@ -0,0 +1,89 @@ +.code-page .sidebar .namespace-header { + display: flex; + flex-direction: row; + gap: 0.75rem; + justify-content: center; + align-items: center; + overflow: hidden; +} + +.code-page .sidebar .namespace-header .icon { + font-size: 1.5rem; + line-height: 1; +} + +.code-page .sidebar .is-overflowing .namespace-header .icon:after { + position: absolute; + top: 0; + right: -1.5rem; + bottom: 0; + content: ""; + width: 1.5rem; + background: linear-gradient( + 90deg, + var(--color-sidebar-bg), + var(--color-sidebar-bg), + var(--color-sidebar-bg-transparent) + ); +} + +.code-page .sidebar .namespace-header .namespace { + display: inline-flex; + color: var(--color-sidebar-fg-em); + font-size: 1rem; + font-weight: 500; + height: 1.5rem; + overflow: hidden; + white-space: nowrap; + text-align: right; + flex-direction: row-reverse; +} + +.code-page .sidebar .sidebar_collapsed .namespace-header { + gap: 0.25rem; +} + +.code-page .sidebar .sidebar_collapsed .namespace-header .icon { + font-size: var(--font-size-base); +} + +.code-page .sidebar .sidebar_collapsed .namespace-header .namespace { + font-size: var(--font-size-small); +} + +.code-page .sidebar .perspective-actions { + display: flex; + flex-direction: row; + gap: 0.375rem; + flex-grow: 1; +} + +.code-page .sidebar .perspective-actions .download, +.code-page .sidebar .perspective-actions .download .button { + flex-grow: 1; +} + +.code-page .sidebar .perspective-actions .download .button .icon { + flex-grow: 0; +} + +.code-page .sidebar .perspective-actions .tooltip { + margin-left: -0.3rem; + margin-top: 0.5rem; +} + +.code-page .sidebar .perspective-actions .tooltip-bubble { + min-width: auto; + font-weight: bold; + font-size: var(--font-size-small); +} + +.code-page .sidebar .perspective-actions .tooltip .fully-qualified-name { + padding: 0; + height: initial; +} + +.code-page .perspective-tree-divider { + display: flex; + flex-grow: 1; +} diff --git a/src/css/unison-share/page/error-page.css b/src/css/unison-share/page/error-page.css new file mode 100644 index 00000000..fd41d5c8 --- /dev/null +++ b/src/css/unison-share/page/error-page.css @@ -0,0 +1,6 @@ +.error-page .card { + margin: auto; +} +.error-page .card .subtle { + font-size: var(--font-size-medium); +} diff --git a/src/css/unison-share/page/project-branches-page.css b/src/css/unison-share/page/project-branches-page.css new file mode 100644 index 00000000..1ef17d8c --- /dev/null +++ b/src/css/unison-share/page/project-branches-page.css @@ -0,0 +1,113 @@ +.project-branches-page .project-branches_list { + display: flex; + flex-direction: column; + width: 100%; + gap: 0.5rem; + margin-left: -0.25rem; +} + +.project-branches-page .project-branches_branch-row { + height: 1.5rem; + font-size: var(--font-size-medium); + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding-left: 0.25rem; + border-radius: var(--border-radius-base); +} + +.project-branches-page .project-branches_branch-row:hover { + background: var(--u-color_container_hovered); +} + +.project-branches-page .project-branches_branch-info { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75rem; +} + +.project-branches-page .project-branches_branch-info .branch-updated-at { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + color: var(--u-color_text_subdued); + line-height: 1; +} + +.project-branches-page + .project-branches_branch-info + .branch-updated-at_tooltip { + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +#delete-branch-modal { + width: 30rem; +} + +#delete-branch-modal .delete-branch-modal_content { + display: flex; + flex-direction: column; + gap: 1.5rem; + position: relative; +} + +#delete-branch-modal p { + margin: 0; +} + +#delete-project-modal .status-banner { + font-weight: bold; +} + +#delete-branch-modal .delete-branch-modal_actions { + display: flex; + flex-direction: row; + gap: 0.5rem; + justify-content: flex-end; +} + +#delete-branch-modal .delete-branch-modal_actions .status-banner { + position: absolute; + left: 0; + z-index: var(--layer-base); +} + +#delete-branch-modal .delete-branch-modal_overlay-deleting { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--color-modal-bg); + opacity: 0.85; + content: " "; +} + +#delete-branch-modal:has(.delete-branch-modal_overlay-success) { + height: 18rem; +} + +#delete-branch-modal .delete-branch-modal_overlay-success { + position: absolute; + top: -4rem; + left: -1rem; + right: -1rem; + bottom: 0; + background: var(--color-modal-bg); + content: " "; + display: flex; + flex-direction: column; + gap: 1.5rem; + align-items: center; + justify-content: center; + text-align: center; + font-size: var(--font-size-base); + line-height: 1.5; + padding-top: 6rem; +} diff --git a/src/css/unison-share/page/project-contribution-changes-page.css b/src/css/unison-share/page/project-contribution-changes-page.css new file mode 100644 index 00000000..c7a47967 --- /dev/null +++ b/src/css/unison-share/page/project-contribution-changes-page.css @@ -0,0 +1,158 @@ +.project-contribution-changes-page { + & .card.changes { + gap: 1.5rem; + } + + & a:hover * { + color: var(--u-color_interactive_hovered); + } + + & .contribution-diff-group { + display: flex; + flex-direction: column; + gap: 0.125rem; + font-size: var(--font-size-medium); + + & .icon { + font-size: var(--font-size-medium); + line-height: 1; + } + + & .diff-line { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + line-height: 1; + position: relative; + + & .tooltip { + left: -0.55rem; + } + + & .diff-icon { + width: 1.125rem; + height: 1.125rem; + border-radius: 0.5625rem; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + } + + & .diff-icon.added { + background: var(--color-green-5); + + & .icon { + color: var(--color-green-1); + } + } + + & .diff-icon.updated { + background: var(--color-orange-5); + + & .icon { + color: var(--color-orange-0); + } + } + + & .diff-icon.removed { + background: var(--color-pink-5); + + & .icon { + color: var(--color-pink-1); + } + } + + & .diff-icon.renamed { + background: var(--color-blue-5); + + & .icon { + color: var(--color-blue-2); + } + } + + & .diff-icon.aliased { + background: var(--color-purple-5); + + & .icon { + color: var(--color-purple-2); + } + } + + & .def-icon-anchor { + position: relative; + + & .def-icon { + width: 1.125rem; + height: 1.125rem; + border-radius: 0.5625rem; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + } + } + + & .icon.term, + & .icon.type, + & .icon.ability, + & .icon.ability-constructor, + & .icon.data-constructor, + & .icon.doc, + & .icon.test { + color: var(--u-color_icon_subdued); + } + + & .diff-info { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + color: var(--u-color_text); + + & .prefix { + color: var(--u-color_text_subdued); + font-family: var(--font-monospace); + } + + & .fully-qualified-name { + font-family: var(--font-monospace); + font-weight: normal; + } + + & .extra-info { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + color: var(--u-color_text_subdued); + font-size: var(--font-size-small); + } + } + } + } + + & .diff-line.namespace { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-top: 0.875rem; + gap: 0.25rem; + + & .namespace-info { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + + & .icon { + color: var(--u-color_icon_subdued); + } + } + + & .contribution-diff-group { + padding-left: 1rem; + } + } +} diff --git a/src/css/unison-share/page/project-contribution-overview-page.css b/src/css/unison-share/page/project-contribution-overview-page.css new file mode 100644 index 00000000..1b460235 --- /dev/null +++ b/src/css/unison-share/page/project-contribution-overview-page.css @@ -0,0 +1,108 @@ +.project-contribution-overview-page { + display: flex; + flex-direction: column; + gap: 1.5rem; + + & .contribution-description { + z-index: 1; + + & .no-description { + font-size: var(--font-size-medium); + color: var(--u-color_text_subdued); + } + + & .definition-doc { + max-width: var(--readable-column-width-medium); + --c-width_doc_inner-content: calc( + var(--readable-column-width-medium) - 3rem + ); + overflow-x: hidden; + } + + & .actions { + border-top: 1px solid var(--u-color_border_subdued); + padding: 1.5rem 1.5rem 0; + margin: 0.5rem -1.5rem 0; + display: flex; + flex: 1; + width: calc(100% + 3rem); + flex-direction: row; + justify-content: space-between; + + & .left-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + } + + & .right-actions { + display: flex; + flex-direction: row; + gap: 0.5rem; + } + + & .tooltip { + & .tooltip-bubble { + top: 2.5rem; + left: 0; + transform: translateX(-75%); + white-space: pre; + } + } + } + } + + & #project-contribution-how-to-review-modal .instructions { + display: flex; + flex-direction: column; + gap: 0.5rem; + + & .copy-field { + margin-bottom: 1rem; + } + + & p { + margin-bottom: 0; + } + } + + & .new-comment_form { + position: relative; + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0 1.5rem; + + & .divider { + background: var(--u-color_border_subdued); + margin-bottom: 1rem; + } + + & .comment-actions { + display: flex; + flex-direction: row; + justify-content: flex-end; + position: relative; + + & .status-banner { + position: absolute; + left: 0; + z-index: 1; + } + } + } + + & .new-comment_form.new-comment_form_working::after { + content: " "; + background: var(--u-color_background_subdued); + position: absolute; + opacity: 0.75; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 0; + } +} diff --git a/src/css/unison-share/page/project-contribution-page.css b/src/css/unison-share/page/project-contribution-page.css new file mode 100644 index 00000000..cd66299b --- /dev/null +++ b/src/css/unison-share/page/project-contribution-page.css @@ -0,0 +1,102 @@ +.project-contribution-page { + & .page-content .page-title .page-title_custom-title { + display: flex; + width: 100%; + flex: 1; + } + + & .contribution-page-title { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + } + + & .page-title_pre-title { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + color: var(--u-color_text_subdued); + font-size: var(--font-size-medium); + width: 100%; + + & .contribution-ref_by-at { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + } + + & .contribution-ref { + font-size: 0.875rem; + font-weight: bold; + background: var(--u-color_element); + padding: 0.125rem 0.5rem; + border: 1px solid var(--u-color_border_subdued); + border-radius: 1.5rem; + } + } + + & .page-title_description { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + color: var(--u-color_text_subdued); + font-size: var(--font-size-medium); + + & .from-to { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75ch; + line-height: 1; + } + + & .time-ago { + text-transform: capitalize; + } + + & .branches { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75ch; + line-height: 1; + + & .tag { + /* @todo @color-todo */ + background: var(--u-color_container); + border: 1px solid var(--u-color_border); + } + & .tag:hover { + border-color: var(--u-color_border_hovered); + background: var(--u-color_container_hovered); + } + } + } +} + +@media only screen and (--u-viewport_max-sm) { + .project-contribution-page { + & .page-title { + flex: 1; + + & .page-title_default-title .text { + gap: 0.5rem; + } + } + + & .page-title_description { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + & .page-title_right-side { + align-self: flex-end; + } + } +} diff --git a/src/css/unison-share/page/project-contributions-page.css b/src/css/unison-share/page/project-contributions-page.css new file mode 100644 index 00000000..d5e122fc --- /dev/null +++ b/src/css/unison-share/page/project-contributions-page.css @@ -0,0 +1,59 @@ +.project-contributions-page { + & .page-title .submit-contribution-disabled .tooltip { + margin-top: 1rem; + } + + & .contributions-empty-state_icon { + height: 4rem; + width: 4rem; + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + + & .icon { + font-size: 2rem; + line-height: 1; + } + } + + & .project-contributions { + display: flex; + flex-direction: column; + gap: 1.5rem; + + & .contribution-row { + display: flex; + flex-direction: column; + gap: 0.35rem; + width: 100%; + + & .contribution-row_header { + display: flex; + flex-direction: row; + justify-content: space-between; + + & .num-comments { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + font-size: var(--font-size-small); + color: var(--u-color_text_subdued); + } + } + + & .contribution-row_ref { + color: var(--u-color_text_subdued); + margin-right: 0.25rem; + } + + & .contribution-row_info { + display: flex; + flex-direction: row; + gap: 0.25rem; + font-size: var(--font-size-medium); + } + } + } +} diff --git a/src/css/unison-share/page/project-overview-page.css b/src/css/unison-share/page/project-overview-page.css new file mode 100644 index 00000000..6686991a --- /dev/null +++ b/src/css/unison-share/page/project-overview-page.css @@ -0,0 +1,198 @@ +.project-overview-page.project-page-loading .page-title { + opacity: 0.25; +} + +.project-overview-page .page-title .kpis { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.project-overview-page .page-title .kpis .is-faved .icon { + color: var(--color-pink-1); +} + +.project-overview-page .column { + gap: 2rem; +} + +.project-overview-page_layout { + display: flex; + flex: 1; + flex-direction: row; + gap: 2rem; +} + +.project-overview-page_content { + flex: 1; +} + +.project-overview-page_sidebar { + display: flex; + width: 12.25rem; /* @todo should match the user sidebar width or similar */ + flex-direction: column; +} + +.project-overview-page_sidebar_loading { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.project-overview-page_sidebar .project-summary { + display: flex; + flex-direction: column; + gap: 1rem; + font-size: var(--font-size-medium); +} + +.project-overview-page .empty-state .project-ref { + font-size: 1.125rem; +} + +.project-overview-page .project-overview-page_empty-state_center-piece { + display: flex; + border-radius: calc(var(--border-radius-base) + 0.25rem); + border: 0.25rem solid var(--u-color_element_emphasized); +} + +.project-overview-page + .project-overview-page_empty-state_center-piece + .hashvatar { + width: 4rem; + height: 4rem; +} + +.project-overview-page .project-description { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.project-overview-page .project-description-empty-state { + border: 1px dashed var(--u-color_border_subdued); + padding: 1rem; + text-align: center; + border-radius: var(--border-radius-base); + font-size: var(--font-size-medium); + font-style: italic; + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: center; + color: var(--u-color_text_subdued); +} + +#edit-project-description-modal { + width: 24rem; +} + +#edit-project-description-modal .edit-project-description-modal_content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +#edit-project-description-modal .edit-project-description-modal_content p { + margin: 0; +} + +#edit-project-description-modal .description-form { + position: relative; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +#edit-project-description-modal .actions { + display: flex; + flex-direction: row; + justify-content: flex-end; + position: relative; +} + +#edit-project-description-modal .actions .status-banner { + position: absolute; + left: 0; +} + +#edit-project-description-modal .actions .buttons { + display: flex; + flex-direction: row; + gap: 0.75rem; +} + +#edit-project-description-modal .saving .description-form:after { + position: absolute; + content: ""; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.85); + border-radius: var(--border-radius-base); +} + +#edit-project-description-modal .save-success { + display: flex; + flex: 1; + place-items: center; + place-content: center; + height: 21.5rem; +} + +.project-overview-page .project-dependencies { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +/* @todo @color-todo */ +.project-overview-page .project-dependencies .tag { + background: var(--u-color_element); + border: 1px solid var(--u-color_border); +} + +#project-readme-instructions-modal { + width: 34.5rem; +} + +#project-readme-instructions-modal .steps { + margin-top: 1.5rem; +} + +#project-readme-instructions-modal .modal-actions { + margin-top: 1.5rem; + display: flex; + justify-content: flex-end; +} + +#project-readme-instructions-modal .pull-hint { + display: flex; + flex-direction: row; + gap: 0.5rem; + margin-top: 0.75rem; + color: var(--u-color_text_subdued); +} + +#project-readme-instructions-modal .pull-hint p:last-child { + margin: 0; +} + +#project-readme-instructions-modal .pull-hint .icon { + color: var(--u-color_icon_subdued); + flex-shrink: 0; + margin-top: 2px; +} + +#project-readme-instructions-modal .pull-hint .inline-code { + background: var(--color-gray-lighten-60); + border-radius: var(--border-radius-base); + padding: 0 0.3rem; +} + +@media only screen and (--u-viewport_max-sm) { + .project-overview-page .page-title .page-title_right-side { + display: none; + } +} diff --git a/src/css/unison-share/page/project-page.css b/src/css/unison-share/page/project-page.css new file mode 100644 index 00000000..80ff0bab --- /dev/null +++ b/src/css/unison-share/page/project-page.css @@ -0,0 +1,94 @@ +.project-page .page-header .page-header_right-side .kpis { + display: flex; + flex-direction: row; + margin-right: 0.75rem; + align-items: center; + position: relative; +} + +.project-page .page-header .page-header_right-side .kpi { + display: flex; + position: relative; +} + +.project-page .page-header .page-header_right-side .kpi .tooltip { + margin-top: 2px; + left: -0.5rem; + pointer-events: none; +} + +.project-page .page-header .page-header_right-side .kpi-content { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + line-height: var(--font-size-medium); + padding: 0.325rem; +} + +.project-page .page-header .page-header_right-side .kpi:hover .kpi-content { + /* @color-todo */ + background: var(--color-gray-darken-30); + border-radius: var(--border-radius-base); +} + +.project-page .page-header .page-header_right-side .kpi .kpi-num { + margin-top: 1px; + font-size: var(--font-size-medium); + font-family: var(--font-monospace); + color: var(--c-color_page-header_text_subdued); + font-weight: bold; +} + +.project-page .page-header .page-header_right-side .kpi .icon { + font-size: 1.25rem; + color: var(--c-color_page-header_icon_subdued); +} + +.project-page .page-header .page-header_right-side a .kpi, +.project-page .page-header .page-header_right-side a .kpi .icon, +.project-page .page-header .page-header_right-side a .kpi .kpi-num, +.project-page .page-header .page-header_right-side a .kpi .kpi-label { + cursor: pointer; +} + +.project-page .page-header .page-header_right-side a.is-faved .icon { + color: var(--color-pink-1); +} + +.project-page .page-header .page-header_right-side a.just-faved .icon { + animation: 0.4s pulsate-size; +} + +.project-page .project-page_login-tip { + display: flex; + flex: 1; + width: 100%; + align-items: center; + justify-content: center; +} + +.project-page .project-page_login-tip .status-banner { + width: fit-content; +} + +.project-page .empty-state-content { + padding: 0 4rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.project-page .empty-state-content code { + background: var(--color-gray-lighten-55); + border-radius: var(--border-radius-base); + padding: 0 0.25rem; + text-align: left; +} + +.project-page .empty-state-content .delete-project { + margin-top: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; +} diff --git a/src/css/unison-share/page/project-release-page.css b/src/css/unison-share/page/project-release-page.css new file mode 100644 index 00000000..236ca444 --- /dev/null +++ b/src/css/unison-share/page/project-release-page.css @@ -0,0 +1,23 @@ +.project-release-page .project-release_page-title_description { + margin-top: 0.25rem; + display: flex; + flex-direction: row; + gap: 0.5rem; + align-content: center; +} + +.project-release-page .project-release_page-title_description .hash, +.project-release-page .project-release_page-title_description .by-at { + font-size: var(--font-size-medium); +} + +.project-release-page .project-release_page-title_description .hash .icon { + font-size: var(--font-size-base); +} + +.project-release-page .project-release_actions { + display: flex; + flex-direction: row; + gap: 0.75rem; + justify-content: flex-end; +} diff --git a/src/css/unison-share/page/project-releases-page.css b/src/css/unison-share/page/project-releases-page.css new file mode 100644 index 00000000..1f73527e --- /dev/null +++ b/src/css/unison-share/page/project-releases-page.css @@ -0,0 +1,227 @@ +.project-releases-page .project-releases_releases { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.project-releases-page .release-draft { + display: flex; + flex: 1; + flex-direction: row; + width: 100%; + justify-content: space-between; + font-size: var(--font-size-small); + padding-left: 0.375rem; +} + +.project-releases-page .release-draft_meta { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; +} + +.project-releases-page .project-release-details { + gap: 0; + padding: 0; +} + +.project-releases-page .project-release-details header { + width: 100%; + display: flex; + flex-direction: column; + gap: 0.5rem; + border-bottom: 1px solid var(--u-color_border_subdued); + padding: 1.5rem; +} + +.project-releases-page .project-release-details_version-hash { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.project-releases-page .project-release-details_version-hash .version:hover { + color: var(--u-color_interactive); +} + +.project-releases-page .project-release-details_release-notes { + display: flex; + flex-direction: column; + flex: 1; + border-bottom: 1px solid var(--u-color_border_subdued); + padding: 1.5rem; + width: 100%; +} + +.project-releases-page + .project-release-details_release-notes + #release-notes_container { + max-height: 20rem; + overflow: hidden; +} + +.project-releases-page + .project-release-details_release-notes.shown-in-full + #release-notes_container { + overflow: visible; + max-height: -moz-fit-content; + max-height: fit-content; +} + +.project-releases-page + .project-release-details_release-notes + .show-full-release-notes { + position: relative; + background: var(--u-color_container); +} + +/* overlapping gradient for the content to peek out behind, indicating more is + * below the fold */ +.project-releases-page + .project-release-details_release-notes + .show-full-release-notes:before { + position: absolute; + top: -4.5rem; + left: 0; + right: 0; + content: ""; + margin: 0; + height: 4.5rem; + background: linear-gradient( + 0deg, + var(--u-color_container) 20%, + var(--u-color_container_faded) 80%, + var(--color-transparent) + ); +} + +/* @todo: feels like this should be using + * --readable-column-width: 43rem — 50rem is pretty wide */ +.project-releases-page .project-release-details_release-notes .definition-doc { + --c-width_doc_outer: 50rem; + --c-width_doc_inner-content: 50rem; +} + +.project-releases-page + .project-release-details_release-notes + .definition-doc + .doc-table { + max-width: var(--c-width_doc_outer); +} + +.project-releases-page .project-release-details footer { + display: flex; + gap: 0.75rem; + width: 100%; + padding: 1.5rem; + justify-content: flex-end; +} + +.project-releases-page .project-releases_loading_past-releases { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.project-releases-page .project-releases_loading_past-releases_group { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.project-releases-page .project-releases_loading_past-releases_group header { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.project-releases-page .project-releases_past-releases { + font-size: var(--font-size-medium); + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.625rem; +} + +.project-releases-page .project-releases_past-releases_release { + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; +} + +.project-releases-page .project-releases_past-releases_release .version { + width: 3.5rem; +} + +.project-releases-page .project-releases_past-releases_release_version-and-hash, +.project-releases-page + .project-releases_past-releases_release_status-and-by-at { + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; +} + +.project-releases-page + .project-releases_past-releases_release_version-and-hash + .version:hover { + color: var(--u-color_interactive); +} + +.project-releases-page .project-releases_error { + align-items: center; + padding: 2rem; +} + +.project-releases-page .browsable-branch-hash { + display: flex; + flex-direction: row; + gap: 0.25rem; + align-items: center; +} +.project-releases-page .browsable-branch-hash * { + transition: none; +} + +.project-releases-page .browsable-branch-hash .icon.browse { + margin-bottom: 1px; +} + +.project-releases-page .browsable-branch-hash:hover, +.project-releases-page .browsable-branch-hash:hover .hash, +.project-releases-page .browsable-branch-hash:hover .icon { + color: var(--u-color_interactive_hovered); +} + +.project-releases-page .browsable-branch-hash:active, +.project-releases-page .browsable-branch-hash:active .hash, +.project-releases-page .browsable-branch-hash:active .icon { + color: var(--u-color_interactive_pressed); +} + +@media only screen and (--u-viewport_max-md) { + .project-releases-page + .project-releases_past-releases_release_version-and-hash, + .project-releases-page + .project-releases_past-releases_release_status-and-by-at { + gap: 0.5rem; + } +} + +@media only screen and (--u-viewport_max-sm) { + .project-releases-page .project-releases_past-releases_release { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .project-releases-page + .project-releases_past-releases_release_version-and-hash, + .project-releases-page + .project-releases_past-releases_release_status-and-by-at { + gap: 0.5rem; + } +} diff --git a/src/css/unison-share/page/project-settings-page.css b/src/css/unison-share/page/project-settings-page.css new file mode 100644 index 00000000..e1bc3f13 --- /dev/null +++ b/src/css/unison-share/page/project-settings-page.css @@ -0,0 +1,99 @@ +.project-settings-page .settings-content .card { + position: relative; +} + +.project-settings-page .settings-content h2 { + color: var(--u-color_text_subdued); + margin-bottom: 0.75rem; +} + +.project-settings-page .actions { + margin-top: 1.5rem; + display: flex; + flex-direction: row; + justify-content: flex-end; + position: relative; +} + +.project-settings-page .actions .status-banner { + position: absolute; + left: 0; +} + +.project-settings-page .actions .buttons { + display: flex; + flex-direction: row; + gap: 0.75rem; +} + +.project-settings-page .saving .card:after { + position: absolute; + content: ""; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.85); + border-radius: var(--border-radius-base); +} + +#delete-project-modal { + width: 30rem; +} + +#delete-project-modal .delete-project-modal_content { + display: flex; + flex-direction: column; + gap: 1.5rem; + position: relative; +} + +#delete-project-modal p { + margin: 0; +} + +#delete-project-modal .status-banner { + font-weight: bold; +} + +#delete-project-modal .delete-project-modal_actions { + display: flex; + flex-direction: row; + gap: 0.5rem; + justify-content: flex-end; +} + +#delete-project-modal .delete-project-modal_actions .status-banner { + position: absolute; + left: 0; + z-index: var(--layer-base); +} + +#delete-project-modal .delete-project-modal_overlay-deleting { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--color-modal-bg); + opacity: 0.85; + content: " "; +} + +#delete-project-modal .delete-project-modal_overlay-success { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--color-modal-bg); + content: " "; + display: flex; + flex-direction: column; + gap: 1.5rem; + align-items: center; + justify-content: center; + text-align: center; + font-size: var(--font-size-base); + line-height: 1.5; +} diff --git a/src/css/unison-share/page/project-ticket-page.css b/src/css/unison-share/page/project-ticket-page.css new file mode 100644 index 00000000..342aee34 --- /dev/null +++ b/src/css/unison-share/page/project-ticket-page.css @@ -0,0 +1,157 @@ +.project-ticket-page { + & .page-title .description { + margin: 0; + } + + & .page-title_description { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75ch; + line-height: 1; + + & .time-ago { + text-transform: capitalize; + } + + & .branches { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.75ch; + line-height: 1; + + & .tag { + /* @todo @color-todo */ + background: var(--u-color_container); + border: 1px solid var(--u-color_border); + } + & .tag:hover { + border-color: var(--u-color_border_hovered); + background: var(--u-color_container_hovered); + } + } + } + + & .ticket-description { + z-index: 1; + + & .definition-doc { + max-width: var(--readable-column-width-medium); + --c-width_doc_inner-content: calc( + var(--readable-column-width-medium) - 3rem + ); + overflow-x: hidden; + } + + & .actions { + border-top: 1px solid var(--u-color_border_subdued); + padding: 1.5rem 1.5rem 0; + margin: 0.5rem -1.5rem 0; + display: flex; + flex: 1; + width: calc(100% + 3rem); + flex-direction: row; + justify-content: space-between; + + & .left-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + } + + & .right-actions { + display: flex; + flex-direction: row; + align-self: flex-end; + margin-left: auto; + gap: 0.5rem; + } + + & .tooltip { + & .tooltip-bubble { + top: 2.5rem; + left: 0; + transform: translateX(-75%); + white-space: pre; + } + } + } + } + + & #project-ticket-how-to-review-modal .instructions { + display: flex; + flex-direction: column; + gap: 0.5rem; + + & .copy-field { + margin-bottom: 1rem; + } + + & p { + margin-bottom: 0; + } + } + + & .new-comment_form { + position: relative; + margin-top: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0 1.5rem; + + & .divider { + background: var(--u-color_border_subdued); + margin-bottom: 1rem; + } + + & .comment-actions { + display: flex; + flex-direction: row; + justify-content: flex-end; + position: relative; + + & .status-banner { + position: absolute; + left: 0; + z-index: 1; + } + } + } + + & .new-comment_form.new-comment_form_working::after { + content: " "; + background: var(--u-color_background_subdued); + position: absolute; + opacity: 0.75; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 0; + } +} + +@media only screen and (--u-viewport_max-sm) { + .project-ticket-page { + & .page-title { + flex: 1; + + & .page-title_default-title .text { + gap: 0.5rem; + } + } + + & .page-title_description { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + & .page-title_right-side { + align-self: flex-end; + } + } +} diff --git a/src/css/unison-share/page/project-tickets-page.css b/src/css/unison-share/page/project-tickets-page.css new file mode 100644 index 00000000..5feded62 --- /dev/null +++ b/src/css/unison-share/page/project-tickets-page.css @@ -0,0 +1,55 @@ +.project-tickets-page { + & .tickets-empty-state_icon { + height: 4rem; + width: 4rem; + display: flex; + flex-direction: row; + place-content: center; + align-items: center; + + & .icon { + font-size: 2rem; + line-height: 1; + } + } + + & .project-tickets { + display: flex; + flex-direction: column; + gap: 1.5rem; + + & .ticket-row { + display: flex; + flex-direction: column; + gap: 0.35rem; + width: 100%; + + & .ticket-row_header { + display: flex; + flex-direction: row; + justify-content: space-between; + + & .num-comments { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + font-size: var(--font-size-small); + color: var(--u-color_text_subdued); + } + } + + & .ticket-row_ref { + color: var(--u-color_text_subdued); + margin-right: 0.25rem; + } + + & .ticket-row_info { + display: flex; + flex-direction: row; + gap: 0.25rem; + font-size: var(--font-size-medium); + } + } + } +} diff --git a/src/css/unison-share/page/ucm-connected.css b/src/css/unison-share/page/ucm-connected.css new file mode 100644 index 00000000..5ca34d31 --- /dev/null +++ b/src/css/unison-share/page/ucm-connected.css @@ -0,0 +1,30 @@ +.ucm-connected .column { + max-width: 32rem; + place-items: center; + gap: 2rem; + margin: auto; + margin-top: 3rem; +} + +.ucm-connected h1, +.ucm-connected p { + text-align: center; + line-height: 1.5; + margin-bottom: 0; +} + +.ucm-connected .actions { + display: flex; + flex-direction: row; + gap: 1rem; +} + +@media only screen and (--u-viewport_max-sm) { + .ucm-connected .column { + gap: 1rem; + } + + .ucm-connected .actions { + flex-direction: column; + } +} diff --git a/src/css/unison-share/page/user-contributions-page.css b/src/css/unison-share/page/user-contributions-page.css new file mode 100644 index 00000000..41a76661 --- /dev/null +++ b/src/css/unison-share/page/user-contributions-page.css @@ -0,0 +1,129 @@ +.user-contributions-page .column { + gap: 1rem; +} + +.user-contributions-page .contributions-by-project { + margin-top: 0.75rem; +} + +.user-contributions-page .user-contributions-page_sub-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 2.25rem; +} + +.user-contributions-page .user-contributions-page_kpis { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; +} + +.user-contributions-page .user-contributions-page_sub-header .text-field { + display: flex; + flex-direction: row; + align-items: center; +} + +.user-contributions-page .project-contributions { + display: flex; + flex-direction: column; + gap: 2.5rem; +} + +.user-contributions-page .contributions-for-project { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.user-contributions-page .contribution-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-left: 2rem; +} + +.user-contributions-page .contribution { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; +} + +.project-contributions_loading { + display: flex; + flex-direction: column; + gap: 2.625rem; +} + +.project-contributions_loading_project { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.project-contributions_loading_contributions { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.user-contributions-page .disabled-search-field { + position: relative; + opacity: 0.35; +} + +.user-contributions-page .disabled-search-field::after { + position: absolute; + content: " "; + left: 0; + right: 0; + top: 0; + bottom: 0; +} +.user-contributions-page .no-contribs { + text-align: center; + width: 12rem; +} + +/* @todo :has */ +/*.user-contributions-page .empty-state:has(.empty-state_search) { */ +.user-contributions-page .empty-state.has_empty-state_search { + margin-top: 2.625rem; +} + +@media only screen and (--u-viewport_max-sm) { + .user-contributions-page .user-contributions-page_sub-header { + height: auto; + width: 100%; + } + + .user-contributions-page + .user-contributions-page_sub-header + .disabled-search-field { + width: 100%; + } + + .user-contributions-page .user-contributions-page_sub-header .text-field { + flex-direction: column; + width: 100%; + align-items: flex-start; + } + + .user-contributions-page + .user-contributions-page_sub-header + .text-field + .text-field_input { + width: 100%; + } + + .user-contributions-page + .user-contributions-page_sub-header + .text-field + .help-text { + margin-left: 0.5rem; + } +} diff --git a/src/css/unison-share/page/user-profile-page.css b/src/css/unison-share/page/user-profile-page.css new file mode 100644 index 00000000..60a569a0 --- /dev/null +++ b/src/css/unison-share/page/user-profile-page.css @@ -0,0 +1,156 @@ +.user-profile-page .user-profile_info { + display: flex; + flex-direction: column; +} + +.user-profile-page .user-profile_info-fields { + display: flex; + flex-direction: column; + gap: 0.375rem; + margin-left: 1rem; + margin-top: 2rem; + font-size: var(--font-size-medium); +} + +.user-profile-page .user-profile_info-field { + display: flex; + flex-direction: row; + gap: 0.5rem; + align-items: center; + min-height: 1.5rem; +} + +.user-profile-page .user-profile_info-fields .icon { + font-size: var(--font-size-medium); + color: var(--u-color_icon_subdued); +} + +.user-profile-page .user-profile_main-content { + display: flex; + flex-direction: column; + gap: 1.5rem; + flex: 1; + flex-grow: 1; +} + +.user-profile-empty-state_instructions-banner { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + gap: 0.75rem; + font-size: var(--font-size-small); + color: var(--u-color_text_subdued); + margin-top: 0.75rem; + border-radius: var(--border-radius-base); +} + +.user-profile-page .readme-empty-state { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; +} + +.user-profile-empty-state_welcome-to { + text-align: center; +} + +.user-profile-empty-state_welcome-to p { + margin-bottom: 0; + line-height: 1; +} + +.user-profile-empty-state_welcome-to p:first-child { + margin-bottom: 0.875rem; +} + +.user-profile_this-is-your-space { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + background: var(--u-color_info_container_subdued); + padding: 0.75rem; + padding-top: 1.5rem; + border-radius: var(--border-radius-base); +} + +#user-profile-readme-instructions-modal { + width: 34.5rem; +} +#user-profile-readme-instructions-modal .steps { + margin-top: 1.5rem; +} +#user-profile-readme-instructions-modal .modal-actions { + margin-top: 1.5rem; + display: flex; + justify-content: flex-end; +} + +#user-profile-readme-instructions-modal .pull-hint { + display: flex; + flex-direction: row; + gap: 0.5rem; + margin-top: 0.75rem; + color: var(--u-color_text_subdued); +} + +#user-profile-readme-instructions-modal .pull-hint p:last-child { + margin: 0; +} + +#user-profile-readme-instructions-modal .pull-hint .icon { + color: var(--u-color_icon_subdued); + flex-shrink: 0; + margin-top: 2px; +} + +#user-profile-readme-instructions-modal .pull-hint .inline-code { + background: var(--color-gray-lighten-60); + border-radius: var(--border-radius-base); + padding: 0 0.3rem; +} + +.user-profile-page .projects { + display: flex; + flex-direction: column; + width: 100%; +} + +.user-profile-page .project { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.user-profile-page .project .project-summary { + font-size: var(--font-size-medium); + margin-left: 2rem; +} + +.user-profile-page .project .tags { + margin-left: 2rem; +} + +#edit-user-profile-modal { + width: 32rem; +} + +/* loading */ + +.user-profile-page.user-page_loading .user-profile_info { + gap: 0.5rem; +} + +@media only screen and (--u-viewport_max-lg) { + .user-profile-page .page-title .page-title_right-side { + display: none; + } +} + +@media only screen and (--u-viewport_max-md) { + .user-profile-page .user-profile-page_page-content { + flex-direction: column; + } +} diff --git a/src/css/unison-share/project-contribution-form-modal.css b/src/css/unison-share/project-contribution-form-modal.css new file mode 100644 index 00000000..884cf8a4 --- /dev/null +++ b/src/css/unison-share/project-contribution-form-modal.css @@ -0,0 +1,79 @@ +#project-contribution-form-modal { + width: 40rem; + min-height: 20rem; + + & .form { + display: flex; + flex-direction: column; + gap: 1.5rem; + + & .branches { + display: flex; + flex-direction: row; + gap: 1ch; + align-items: center; + + & .source-branch { + position: relative; + + & .source-branch_invalid { + position: absolute; + top: 2rem; + left: 1rem; + font-size: var(--font-size-small); + font-weight: bold; + color: var(--u-color_critical_text); + white-space: nowrap; + + & .icon { + color: var(--u-color_critical_text); + } + } + } + } + + & .fields { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + & label { + font-weight: bold; + } + } + + & .saved { + padding: 5rem 0; + } + + & .small-print { + background: var(--u-color_container_subdued); + padding: 0.5rem; + border-radius: var(--border-radius-base); + color: var(--u-color_text_subdued); + font-size: var(--font-size-small); + display: flex; + flex-direction: row; + gap: 0.25rem; + + & p { + margin: 0; + } + + & .info-icon { + display: flex; + flex-shrink: 0; + background: var(--u-color_info_element_emphasized); + height: 1rem; + width: 1rem; + border-radius: 0.5rem; + + & .icon { + color: var(--u-color_info_icon-on-element_emphasized); + font-size: var(--font-size-base); + line-height: 1; + } + } + } +} diff --git a/src/css/unison-share/project-ticket-form-modal.css b/src/css/unison-share/project-ticket-form-modal.css new file mode 100644 index 00000000..c3752b9d --- /dev/null +++ b/src/css/unison-share/project-ticket-form-modal.css @@ -0,0 +1,24 @@ +#project-ticket-form-modal { + width: 40rem; + min-height: 20rem; + + & .form { + display: flex; + flex-direction: column; + gap: 1.5rem; + + & .fields { + display: flex; + flex-direction: column; + gap: 1.5rem; + } + + & label { + font-weight: bold; + } + } + + & .saved { + padding: 5rem 0; + } +} diff --git a/src/css/unison-share/project/project-listing.css b/src/css/unison-share/project/project-listing.css new file mode 100644 index 00000000..fce3fcd4 --- /dev/null +++ b/src/css/unison-share/project/project-listing.css @@ -0,0 +1,27 @@ +.project-listing { + --c-color_project-name-listing_private-icon: var( + --u-color_info_icon-on-element_subdued + ); + --c-color_project-name-listing_private-icon_background: var( + --u-color_info_element_subdued + ); + + display: flex; + flex-direction: row; + align-items: center; + gap: 0.5rem; + + & .private-icon { + width: 1.5rem; + height: 1.5rem; + border-radius: 0.75rem; + background: var(--c-color_project-name-listing_private-icon_background); + display: inline-flex; + place-content: center; + place-items: center; + } + + & .private-icon .icon { + color: var(--c-color_project-name-listing_private-icon); + } +} diff --git a/src/css/unison-share/project/project-ref.css b/src/css/unison-share/project/project-ref.css new file mode 100644 index 00000000..cbf2b3f4 --- /dev/null +++ b/src/css/unison-share/project/project-ref.css @@ -0,0 +1,29 @@ +.project-ref { + --c-color_project-ref_handle: var(--u-color_text_subdued); + --c-color_project-ref_separator: var(--u-color_text_very-subdued); + --c-color_project-ref_slug: var(--u-color_text); + + font-weight: bold; + display: inline-flex; + flex-direction: row; + gap: 0.25rem; +} + +.project-ref .project-ref_handle { + color: var(--c-color_project-ref_handle); + white-space: nowrap; +} + +.project-ref .project-ref_separator { + color: var(--c-color_project-ref_separator); +} + +.project-ref .project-ref_slug { + color: var(--c-color_project-ref_slug); + white-space: nowrap; +} + +.project-ref a.project-ref_handle:hover, +.project-ref a.project-ref_slug:hover { + color: var(--u-color_interactive_hovered); +} diff --git a/src/css/unison-share/publish-project-release-modal.css b/src/css/unison-share/publish-project-release-modal.css new file mode 100644 index 00000000..2482877a --- /dev/null +++ b/src/css/unison-share/publish-project-release-modal.css @@ -0,0 +1,306 @@ +#publish-project-release-modal { + width: 53rem; +} + +#publish-project-release-modal .modal-content { + height: 30rem; + position: relative; +} + +#publish-project-release-modal .help { + color: var(--u-color_text_subdued); +} + +#publish-project-release-modal .publish-project-release_content { + display: flex; + flex-direction: column; + gap: 1.5rem; + position: relative; + flex: 1; + height: 100%; +} + +#publish-project-release-modal .publish-project-release_content_disabled:after { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--color-modal-bg); + opacity: 0.85; + content: " "; +} + +#publish-project-release-modal .publish-project-release_form-and-release-notes { + display: flex; + flex-direction: row; + gap: 2rem; + height: 100%; +} + +#publish-project-release-modal .publish-project-release_form { + display: flex; + flex-direction: column; + gap: 1.5rem; + width: 17rem; + flex-shrink: 0; +} + +#publish-project-release-modal .publish-project-release_form > section { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +#publish-project-release-modal .publish-project-release_release-notes { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding-left: 2rem; + border-left: 1px solid var(--u-color_border_subdued); + width: 31rem; +} + +#publish-project-release-modal .publish-project-release_footer { + display: flex; + flex-grow: 1; + flex-direction: row; + gap: 0.5rem; + align-items: center; + justify-content: flex-end; +} + +#publish-project-release-modal .publish-project-release_footer .status-banner { + position: absolute; + left: 0; + z-index: var(--layer-base); +} + +/* Success State */ + +#publish-project-release-modal .publish-project-release_publish-success { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + align-self: center; + justify-self: center; + gap: 1rem; + font-size: var(--font-size-base); + line-height: 1; + margin-top: 2.625rem; + padding-top: 8rem; + height: 18rem; + background: url("assets/confetti.svg") top center no-repeat; +} + +#publish-project-release-modal .publish-project-release_publish-success_emoji { + font-size: 2.625rem; + width: 4.5rem; + height: 4.5rem; + border-radius: 2.25rem; + background: var(--color-purple-5); + display: flex; + align-items: center; + justify-content: center; +} + +#publish-project-release-modal .publish-project-release_publish-success h1 { + font-size: var(--font-size-large); + font-weight: 900; +} + +#publish-project-release-modal .release-notes-preview { + position: relative; + background: var(--color-modal-bg); + border: 1px solid var(--u-color_border_subdued); + border-radius: var(--border-radius-base); + padding: 0.75rem; + display: flex; + flex-direction: column; + max-height: 19rem; + gap: 1.5rem; + overflow: hidden; +} + +#publish-project-release-modal .release-notes-preview:has(.definition-doc) { + height: 100%; +} + +#publish-project-release-modal + .publish-project-release_release-notes-preview-maximized + .release-notes-preview { + position: absolute; + max-height: none; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +#publish-project-release-modal + .publish-project-release_release-notes-preview-maximized + .release-notes-preview { + overflow: auto; + overflow-x: hidden; +} + +#publish-project-release-modal + .release-notes-preview + .release-notes-preview_header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + z-index: var(--layer-base); +} + +#publish-project-release-modal + .release-notes-preview + .release-notes-preview_title { + color: var(--u-color_text_subdued); + display: flex; + flex-direction: row; + gap: 0.25rem; + align-items: center; + line-height: 1; + white-space: nowrap; +} + +#publish-project-release-modal + .release-notes-preview + .release-notes-preview_title + .icon { + color: var(--u-color_icon_subdued); +} + +#publish-project-release-modal + .release-notes-preview + .release-notes-preview_loading { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +#publish-project-release-modal + .release-notes-preview + .release-notes-preview_loading_items { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +#publish-project-release-modal .release-notes-preview .definition-doc { + display: flex; + flex-direction: column; + font-size: var(--font-size-medium); + --c-width_doc_outer: 28rem; + --c-width_doc_inner-content: 28rem; +} + +/* resizing the result indicator to better fit with the smaller preview padding */ +#publish-project-release-modal + .release-notes-preview + .definition-doc + .result-indicator { + width: 1rem; + height: 1rem; + left: -0.25rem; + top: -0.5rem; +} + +#publish-project-release-modal + .release-notes-preview + .definition-doc + .result-indicator + .icon { + font-size: var(--font-size-small); +} + +#publish-project-release-modal .release-notes-preview:before { + content: " "; + width: 2rem; + position: absolute; + top: 0; + bottom: 0; + right: 0; + background: linear-gradient( + 270deg, + var(--color-modal-bg) 20%, + var(--color-modal-bg-faded) 80%, + var(--color-transparent) + ); +} + +#publish-project-release-modal .release-notes-preview:after { + content: " "; + height: 2rem; + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient( + 0deg, + var(--color-modal-bg) 20%, + var(--color-modal-bg-faded) 80%, + var(--color-transparent) + ); +} + +#publish-project-release-modal + .publish-project-release_release-notes-preview-maximized + .release-notes-preview:after, +#publish-project-release-modal + .publish-project-release_release-notes-preview-maximized + .release-notes-preview:before { + display: none; +} + +/* @todo: feels like this should be using + * --readable-column-width: 43rem — 50rem is pretty wide */ +#publish-project-release-modal + .publish-project-release_release-notes-preview-maximized + .release-notes-preview + .definition-doc { + --c-width_doc_outer: 50rem; + --c-width_doc_inner-content: 50rem; +} + +#publish-project-release-modal .release-notes-preview .definition-doc aside { + position: relative; + right: auto; + width: auto; + margin: 1rem; +} + +#publish-project-release-modal + .release-notes-preview + .definition-doc + .doc-table { + max-width: var(--c-width_doc_outer); +} + +/* Loading State */ + +#publish-project-release-modal .publish-project-release_loading { + display: flex; + flex-grow: 1; + flex-direction: column; + gap: 2rem; +} + +#publish-project-release-modal .publish-project-release_loading_group { + display: flex; + flex-grow: 1; + flex-direction: column; + gap: 0.75rem; +} + +/* Error State */ + +#publish-project-release-modal .publish-project-release_error { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} diff --git a/src/css/unison-share/readme-card.css b/src/css/unison-share/readme-card.css new file mode 100644 index 00000000..7c55d4e4 --- /dev/null +++ b/src/css/unison-share/readme-card.css @@ -0,0 +1,29 @@ +.readme-card .definition-doc { + --c-width_doc_outer: 44.265rem; + --c-width_doc_inner-content: 44.265rem; +} + +/* @todo: feels like this should be using + * --readable-column-width: 43rem — 50rem is pretty wide */ +.page-content :not(.with-page-aside) .readme-card .definition-doc { + --c-width_doc_outer: 50rem; + --c-width_doc_inner-content: 50rem; +} + +.readme-card .definition-doc aside { + width: 14rem; + right: -16rem; +} + +.readme-card .definition-doc .doc-table { + max-width: var(--c-width_doc_outer); +} + +@media only screen and (--u-viewport_max-xl) { + .readme-card .definition-doc aside { + position: relative; + right: auto; + width: auto; + margin: 1.5rem; + } +} diff --git a/src/css/unison-share/report-bug-modal.css b/src/css/unison-share/report-bug-modal.css new file mode 100644 index 00000000..326ae102 --- /dev/null +++ b/src/css/unison-share/report-bug-modal.css @@ -0,0 +1,21 @@ +#report-bug-modal { + width: 32.5rem; +} + +#report-bug-modal .actions .button { + margin-right: 0.5rem; +} + +#report-bug-modal .actions .action { + display: flex; + align-items: center; + margin-bottom: 1rem; +} + +#report-bug-modal .actions .action strong { + margin: 0 0.25rem; +} + +#report-bug-modal .actions .action:last-child { + margin-bottom: 0; +} diff --git a/src/css/unison-share/search-branch-sheet.css b/src/css/unison-share/search-branch-sheet.css new file mode 100644 index 00000000..ebcce546 --- /dev/null +++ b/src/css/unison-share/search-branch-sheet.css @@ -0,0 +1,93 @@ +.search-branch-sheet { + width: 14rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.search-branch-sheet search { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* @color-todo @inverse */ +.search-branch-sheet .tag { + --c-color_tag_bg_hovered: var(--color-gray-darken-10); +} + +.search-branch-sheet .search-branch-sheet_branches { + position: relative; + min-height: 3rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.search-branch-sheet + .search-branch-sheet_branches + .search-branch-sheet_suggestions { + position: relative; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.search-branch-sheet_recent-branches_loading { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.search-branch-sheet_branches_loading-section { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.search-branch-sheet_branches_loading-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.search-branch-sheet_branch-list_items { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.search-branch-sheet_error { + color: var(--u-color_critical_text); + display: flex; + flex-direction: row; + gap: 0.5rem; + align-items: center; +} + +.search-branch-sheet .search-branch-sheet_error .icon { + color: var(--u-color_critical_icon); +} + +.search-branch-sheet_searching { + position: absolute; + background: var(--c-color_anchored-overlay_sheet_bg); + top: 0; + bottom: 0; + left: 0; + right: 0; + opacity: 0.85; +} + +.search-branch-sheet_no-results_message { + display: flex; + flex-direction: column; + gap: 0.25rem; + text-align: center; + text-wrap: balance; + color: var(--u-color_text); +} + +.search-branch-sheet_no-results_message p { + margin: 0; +} diff --git a/src/css/unison-share/setup-instructions.css b/src/css/unison-share/setup-instructions.css new file mode 100644 index 00000000..1de7721c --- /dev/null +++ b/src/css/unison-share/setup-instructions.css @@ -0,0 +1,79 @@ +.setup-instructions .inline-code { + background: var(--color-gray-lighten-60); + border-radius: var(--border-radius-base); + padding: 0.1rem 0.375rem; +} + +.setup-instructions .example-namespace-structure { + display: flex; + margin: 1rem 0; + padding-left: 1.5rem; +} + +.setup-instructions .copy-field { + margin-top: 1rem; +} + +.setup-instructions .browser-hint { + display: flex; + flex-direction: row; + gap: 0.5rem; + margin-top: 0.75rem; + color: var(--u-color_text); +} + +.setup-instructions .browser-hint p:last-child { + margin: 0; +} + +.setup-instructions .browser-hint .icon { + color: var(--u-color_icon_subdued); + flex-shrink: 0; + margin-top: 2px; +} + +.setup-instructions .browser-hint .inline-code { + background: var(--color-gray-lighten-60); + border-radius: var(--border-radius-base); + padding: 0 0.3rem; +} + +.setup-instructions .code { + border-radius: var(--border-radius-base); + background: var(--u-color_container_subdued); + overflow: auto; + padding: 0.25rem 0.5rem; + width: 34rem; + scrollbar-width: auto; + scrollbar-color: var(--color-gray-lighten-30) var(--color-transparent); +} + +.setup-instructions .code::-webkit-scrollbar { + height: 0.375rem; +} + +.setup-instructions .code::-webkit-scrollbar-track { + background: var(--color-transparent); +} + +.setup-instructions .code::-webkit-scrollbar-thumb { + background-color: var(--color-gray-lighten-30); + border-radius: var(--border-radius-base); +} + +.setup-instructions .install-instructions { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.setup-instructions ol { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-left: 2.5rem; +} + +.setup-instructions ul { + margin-left: 1.5rem; +} diff --git a/src/css/unison-share/timeline.css b/src/css/unison-share/timeline.css new file mode 100644 index 00000000..264cec8e --- /dev/null +++ b/src/css/unison-share/timeline.css @@ -0,0 +1,111 @@ +.timeline { + overflow: hidden; + padding: 0 1.5rem; + display: flex; + flex-direction: column; + gap: 1.5rem; + + & .timeline-event { + display: flex; + flex-direction: column; + gap: 0.75rem; + font-size: var(--font-size-medium); + + & .timeline-event_header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + line-height: 1; + + & .timeline-event_header_description { + display: flex; + flex-direction: row; + gap: 0.75rem; + align-items: center; + } + + & .timeline-event_icon { + background: var(--color-gray-lighten-45); + height: 1.5rem; + width: 1.5rem; + border-radius: 0.75rem; + display: flex; + align-items: center; + justify-content: center; + position: relative; + + & .icon { + position: relative; + z-index: 2; + color: var(--color-gray-darken-20); + } + } + + & .timeline-event_icon::before { + position: absolute; + content: " "; + background: var(--color-gray-lighten-45); + width: 1px; + height: 500px; + bottom: 1.5rem; + left: calc(50% - 1px); + z-index: 0; + } + + & .timeline-event_icon::after { + position: absolute; + content: " "; + z-index: 1; + top: 0; + left: 0; + height: 1.5rem; + width: 1.5rem; + border-radius: 1.5rem; + padding: 0; + box-shadow: 0 0 0 4px var(--u-color_background_subdued); + } + + & .timeline-event_description { + display: flex; + flex-direction: row; + gap: 0.25rem; + } + + & .event-actions { + display: flex; + flex-direction: row; + gap: 0.25rem; + align-items: center; + } + } + + & .card { + background: var(--u-color_element); + padding: 0.5rem 0.75rem; + + &.comment-event_content { + margin-top: 0.5rem; + margin-left: 2rem; + } + + & .definition-doc { + --color-doc-bg: var(--u-color_element); + } + } + + & .comment-event_edit { + margin-top: 0.5rem; + margin-left: 2rem; + } + } +} + +@media only screen and (--u-viewport_max-sm) { + .timeline { + & .timeline-event { + flex-direction: column; + gap: 0.75rem; + } + } +} diff --git a/src/css/unison-share/use-project-modal.css b/src/css/unison-share/use-project-modal.css new file mode 100644 index 00000000..a00c37d7 --- /dev/null +++ b/src/css/unison-share/use-project-modal.css @@ -0,0 +1,77 @@ +#use-project-modal { + width: 38rem; + + & h3 { + font-size: var(--font-size-base); + } + + & p { + margin-bottom: 0; + } + + & .use-project-modal_content { + display: flex; + flex-direction: column; + gap: 2rem; + } + + & .instruction { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + & .hint { + padding-left: calc(var(--border-radius-base) / 2); + } + + & .upgrade-hint { + padding-left: calc(var(--border-radius-base) / 2); + margin-top: 0.5rem; + font-size: var(--font-size-small); + background: var(--u-color_container_subdued); + padding: 0.5rem; + border-radius: var(--border-radius-base); + display: flex; + flex-direction: row; + gap: 0.5rem; + } + + & .upgrade-hint .upgrade-icon { + height: 1rem; + width: 1rem; + background: var(--u-color_element); + border: 1px solid var(--u-color_border_subdued); + border-radius: 0.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + } + + & .upgrade-hint .icon { + font-size: var(--font-size-small); + line-height: 1; + } + + & .upgrade-hint .upgrade-hint_content { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + & .upgrade-hint .monospace { + font-family: var(--font-monospace); + } + + & .action { + display: flex; + justify-content: flex-end; + margin-top: 1.5rem; + } +} + +@media only screen and (--u-viewport_max-sm) { + #use-project-modal { + width: calc(100vw - 2rem); + } +} diff --git a/src/css/unison-share/welcome-tour-modal.css b/src/css/unison-share/welcome-tour-modal.css new file mode 100644 index 00000000..817051a5 --- /dev/null +++ b/src/css/unison-share/welcome-tour-modal.css @@ -0,0 +1,135 @@ +#welcome-tour-modal { + width: 52rem; +} + +#welcome-tour-modal h2 { + display: flex; + align-items: center; + line-height: 1; + gap: 0.5rem; + font-size: 1.125rem; + margin-bottom: 1.5rem; +} + +#welcome-tour-modal hr { + margin-top: 0.5rem; +} + +#welcome-tour-modal h2 .icon { + font-size: 1.125rem; + color: var(--u-color_icon_subdued); +} + +#welcome-tour-modal ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +#welcome-tour-modal ul li { + display: flex; + flex-direction: row; + gap: 0.5rem; + align-items: center; +} + +#welcome-tour-modal ul li .icon { + color: var(--u-color_icon_subdued); +} + +#welcome-tour-modal .welcome { + display: flex; + flex-direction: row; + font-size: var(--font-size-base); +} + +#welcome-tour-modal .welcome p { + /* helps the text to be aligned with the button */ + display: flex; + align-items: center; +} + +#welcome-tour-modal .welcome .button { + margin-left: 0.5rem; +} + +#welcome-tour-modal .welcome .avatar-container { + width: 8rem; + display: flex; + justify-content: center; + flex-shrink: 0; +} + +#welcome-tour-modal .welcome .avatar { + --avatar-size: 5.25rem; + width: var(--avatar-size); + height: var(--avatar-size); + border-radius: calc(var(--avatar-size) / 2); +} + +#welcome-tour-modal .membership-tenets { + display: flex; + flex-direction: row; + gap: 1.5rem; +} + +#welcome-tour-modal .membership-tenets .tenets { + display: flex; + flex-direction: column; +} + +#welcome-tour-modal .membership-tenets .tenets ul { + margin-left: 1rem; +} + +#welcome-tour-modal .documents { + background: var(--u-color_container_subdued); + padding: 1.5rem; + border-radius: var(--border-radius-base); + min-width: 14rem; + height: fit-content; +} + +#welcome-tour-modal footer { + margin-top: 3rem; + display: flex; + justify-content: flex-end; +} + +@media only screen and (--u-viewport_max-lg) { + #welcome-tour-modal { + width: 48rem; + max-height: calc(100vh - 8rem); + } +} + +@media only screen and (--u-viewport_max-md) { + #welcome-tour-modal { + overflow-y: auto; + width: calc(100vw - 2rem); + } +} + +@media only screen and (--u-viewport_max-sm) { + #welcome-tour-modal { + margin-top: 2rem; + } + + #welcome-tour-modal .welcome { + font-size: var(--font-size-medium); + } + + #welcome-tour-modal .welcome .avatar-container { + display: none; + } + + #welcome-tour-modal .membership-tenets { + flex-direction: column; + } + + #welcome-tour-modal footer { + justify-content: center; + margin-top: 1.5rem; + } +} diff --git a/src/maintenance.html b/src/maintenance.html new file mode 100644 index 00000000..710e2d5f --- /dev/null +++ b/src/maintenance.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + Unison Share | Down for maintenance + + + + +
+

Unison Share

+

+ We're sorry, but Unison Share is currently down for maintenance. We'll + be back soon! +

+
+
+ + diff --git a/src/metrics.js b/src/metrics.js new file mode 100644 index 00000000..48360d3d --- /dev/null +++ b/src/metrics.js @@ -0,0 +1,31 @@ +import Plausible from "plausible-tracker"; + +let plausible; + +function init() { + if (APP_ENV === "production") { + plausible = Plausible({ + domain: "share.unison-lang.org", + }); + + plausible.enableAutoPageviews(); + } else { + console.log(`[${APP_ENV}] Metrics.init`); + } +} + +function track(evt) { + if (APP_ENV === "production") { + if (evt.data) { + plausible(evt.event, { props: evt.data }); + } else { + plausible(evt.event); + } + } else if (evt.data) { + console.log(`[${APP_ENV}] Metrics.track: ${evt.event} ->`, evt.data); + } else { + console.log(`[${APP_ENV}] Metrics.track: ${evt.event}`); + } +} + +export { init, track }; diff --git a/src/privacy-policy.md b/src/privacy-policy.md new file mode 100644 index 00000000..b2b1e63e --- /dev/null +++ b/src/privacy-policy.md @@ -0,0 +1,180 @@ +## Introduction + +This privacy policy (“**Policy**”) is designed to inform users of Unison Computing, PBC’s (“**Unison**’s” or “**our**”) proprietary cloud computing platform, hosting service, and related websites and applications (individually or collectively, the “**Service**”) about how Unison gathers and uses personal information collected by us in connection with the Service. BY ACCESSING OR USING THE SERVICE, YOU ACKNOWLEDGE THAT YOU HAVE READ, UNDERSTAND, AND AGREE TO BE BOUND BY THIS PRIVACY POLICY. IF YOU DO NOT AGREE TO THESE TERMS, DO NOT USE THE SERVICE. +We will take reasonable steps to protect user privacy consistent with the guidelines set forth in this policy and with applicable U.S. laws, including the California Consumer Privacy Act (“**CCPA**”) and the General Data Protection Regulation (“**GDPR**”). In this policy, “user” or “you” means any person using or otherwise benefiting from the Service or otherwise submitting personal information to Unison. + +## 1. GDPR Disclosures + +### What Information Do We Collect? + +#### Limited Personal Information + +We collect the following personal information in connection with the Service: + +- Name, email addresses and other information obtained from the identity provider (such as GitHub) that you use to establish your account with us +- Internet Protocol address + +All of this information is referred to in this Policy as “**Personal Information**”. + +Apart from the limited personal information described above, Unison does not collect and does not wish to receive any personally identifying information, nor does Unison collect or wish to collect any data from individuals under the age of 18. + +#### What are the Legal Bases for Our Collection and Use of Personal Information? + +We rely on the following legal grounds to collect and process your Personal Information: + +- Performance of a contract – We need to collect and use your Personal Information to perform our agreement with you to deliver the Service as described in this Privacy Policy. +- Consent – We may use or disclose some of your Personal Information as described in this Privacy Policy subject to your consent. +- Legitimate interests – We may use your Personal Information for our legitimate interests to improve our products and services and the Service. Consistent with our legitimate interests and any choices that we offer or consents that may be required under applicable laws, we may use technical information as described in this Privacy Policy. + +### How Do We Use the Information We Collect? + +#### Personal Information + +Your Personal Information may be supplemented with additional information regarding your activities on the Service; to the extent that such information is linked specifically to you, we will treat that additional information as your Personal Information. We may use your email address to contact you to market our materials or for the internal operational and administrative purposes of the Service. + +#### User Data + +We collect and store User Data in order to provide the Service to you, and it may be used for the internal operational, product development, and administrative purposes of the Service. + +#### Web Tracking Information + +We use web tracking information to administer the Service and to understand how well our Service is working, to store your user preferences, and to develop statistical information on usage of the Service. This allows us to determine which features and content users like best to help us improve our Service, to personalize your user experience, and to measure overall effectiveness. + +#### Aggregate Information + +We will also create statistical, aggregated data relating to our users and the Service for analytical purposes. Aggregated data is derived from Personal Information and User Data but in its aggregated form it does not duplicate or reveal any User Data or relate to or identify any individual or entity. + +### What Information Do We Disclose to Third Parties? + +#### Personal Information and User Data + +We will not disclose your Personal Information or User Data to any third parties except as follows: +(i) to third party contractors engaged to provide services on our behalf (“Contractors”), such as performing marketing, analyzing data and usage of the Service, hosting and operating the Service or providing support and maintenance services for the Service, or providing customer service. We enter into agreements with all Contractors that require Contractors to use the Personal Information they receive only to perform services for us; or +(ii) when we have your consent to share the information. + +#### Email Communications + +If you register and provide your email address, we will send you administrative and promotional emails. If you wish to opt out of these emails, you may do so by emailing privacy@unison.cloud. + +#### Network Operators + +Use of the Service may involve use of the services of third party internet or telecommunications providers. Such providers are not our contractors, and any information that a provider collects in connection with your use of the Service is not “Personal Information” and is not subject to this Privacy Policy. We are not responsible for the acts or omissions of these third parties. + +#### Legal Exception + +Notwithstanding the above, we may in any event use Personal Information and other information collected through the Service to the extent required by law or legal process, to resolve disputes, to enforce our agreements (including this Privacy Policy), or if in our reasonable discretion use is necessary to protect our legal rights or to protect third parties. + +#### Additional Disclosures + +We reserve the right to disclose any information we collect in connection with the Service, including Personal Information, (a) to any successor to our business as a result of any merger, acquisition, asset sale or similar transaction; and (b) to any law enforcement, judicial authority, or governmental or regulatory authority, to the extent required by law or if in our reasonable discretion disclosure is necessary to enforce or protect our legal rights or to protect third parties. + +### Your Choices About Your Information + +You have the right to: + +- request an accounting of all Personal Information that we possess that pertains to you in an electronically portable format (e.g., electronic copies of information attached to an email). +- request that we change any Personal Information that pertains to you. +- request that we delete any Personal Information that pertains to you. + +To request an accounting of your Personal Information, a change to your Personal Information, or deletion of your Personal Information, contact privacy@unison.cloud. + +If you are a resident of the European Union and have a complaint about our use or processing of your Personal Information, you have a right to lodge a complaint with a national Data Protection Authority. Each European Union member nation has established its own Data Protection Authority; you can find out about the Data Protection Authority in your country here: [Article 29](http://ec.europa.eu/justice/article-29/structure/data-protection-authorities/index_en.htm). + +If you have consented to the collection, processing, and/or transfer of your Personal Information, you have the right fully or partially to withdraw your consent. To withdraw your consent, please contact privacy@unison.cloud. Once we have received notification that you withdraw your consent, we will no longer process your information for the purposes to which you consented unless there are compelling legitimate grounds for further processing or for the establishment, exercise, or defense of legal claims. + +### General + +#### Security + +We use reasonable security precautions to protect the security and integrity of your Personal Information in accordance with this policy and applicable law. However, no Internet transmission is completely secure, and we cannot guarantee that security breaches will not occur. Without limitation of the foregoing, we are not responsible for the actions of hackers and other unauthorized third parties that breach our reasonable security procedures. + +#### Links + +During the course of using our Service you may encounter links to other websites or services. Unison is not responsible for the privacy practices or the content of those websites. Users should be aware of this when they leave our Service and review the privacy statements of each third party website. This Privacy Policy applies solely to information collected by the Service. + +#### Amendments + +Unison may modify or amend this policy from time to time. If we make any material changes in the way in which Personal Information is collected, used or transferred, we will notify you of these changes by modification of this Privacy Policy, which will be available for review by you at the Service. + +#### Geographic Location + +We offer the service in several geographic regions. We define a geographic region as the location where a user is located. + +##### Users Within the United States + +For users within the United States, we process data in data centers located in the United States. We have adopted reasonable physical, technical, and organizational safeguards against accidental, unauthorized, or unlawful destruction, loss, alteration, disclosure, access, use, or processing of user data in our possession. We comply with state and federal laws governing the protection of personal information. + +##### Users Within the European Union + +For users within the European Union, we transfer data from the European Union to data centers located in the United States for processing. Such processing is performed in accordance with Regulation 2016/679 of the European Parliament and of the Council of April 27, 2016 on the protection of natural persons with regard to the processing of personal data and free movement of such data, known as the General Data Protection Regulation (“**GDPR**”). This includes the imposition of required safeguards with respect to accidental, unauthorized or unlawful destruction, loss, alteration, disclosure, access, use or processing of data. Transfers of European Union user data to processors in the United States are made in accordance with the GDPR. + +##### Users in Other Regions + +For users not within the United States or the European Union, we transfer data from such regions to the United States for processing. For such users, we have adopted reasonable physical, technical, and organizational safeguards against accidental, unauthorized, or unlawful destruction, loss, alteration, disclosure, access, use, or processing of user data in our possession that substantially mirror protections available to users located within the United States. + +### Retention and Deletion + +We will only retain your Personal Information for as long as necessary to fulfill the purposes for which it was collected and processed, including for the purposes of satisfying any legal, regulatory, accounting or reporting requirements. +In some circumstances, we may anonymize your Personal Information so that it can no longer be associated with you, at which point it will no longer be treated as Personal Information. +It is our policy to retain Personal Information for until such personal information is no longer necessary to deliver the Service and to delete such personal information thereafter. + +### About Us + +We are based in the United States of America at Unison Computing, PBC, 177 Huntington Ave, Ste 1703 PMB 30333 Boston, MA, 02115-3153. Users in the United States and regions other than the European Union can contact us at the above address. +Our representative in the EU for GDPR purposes can be contacted at gdpr@unison.cloud. + +## 2. CCPA Disclosures + +### California Users’ Rights + +California residents have certain rights regarding our collection and use of personal information under the California Consumer Privacy Act (“**CCPA**” or the “**Act**”). For purposes of this “CCPA Section,” the term “personal information” means personal information under Cal. Civ. Code 1798.140(o). To the extent that any other provision of this Privacy Policy conflicts a provision of this CCPA Section, the provision in this section controls as to California residents. + +California residents may: + +- Request that we delete their personal information. +- Opt-out of the sale of their personal information. +- Request information about: + - (i) the categories and specific pieces of personal information we have collected about the User; + - (ii) the categories of sources from which we have collected the User’s personal information; + - (iii) the business or commercial purpose for collecting or selling the User’s personal information\*; + - (iv) the categories of personal information we have sold about the User; and + - (v) the categories of third parties with whom we have shared, disclosed for a business purpose, or sold the User’s personal information, and which categories of personal information we have sold to which categories of third parties. + +\*We will not discriminate against Users for exercising their rights under the CCPA. + +### Exercising Your Rights + +#### Requests for Information + +California Users may request that we provide certain information about our use of their personal information, as specified above. Upon receiving a request from a User, we will take reasonable steps to verify the User’s identity. We will respond to a verified User request by: (a) providing the requested information; or (b) explaining to the User why the Act does not require us to provide the requested information. + +We will respond to requests for information within 45 days. If a response requires additional time, we will notify the User of the basis for the delay and may extend our response period up to an additional 90 days. If the Act requires us to provide the information requested to the User, we will provide the information covering the 12 months preceding the request, free of charge, and in a readily useable portable format. We have no obligation to provide personal information to a User more than twice in a 12-month period. If a request or series of requests are manifestly unfounded or excessive, we may charge a reasonable fee for processing the request(s), or may refuse to process the request(s). + +#### Requests to Opt-Out + +California users may opt-out of the sale of their personal information by contacting us at privacy@unison.cloud. + +Once we have received a directive from a User to not sell the User’s personal information, we will not sell the User’s personal information unless and until the User subsequently provides express authorization for the sale of the User’s personal information. We will not ask the User for permission to sell their personal information for 12 months after receiving a request to opt-out. We will only use information that a User provides in submitting a request to opt out for purposes of processing the opt-out request. + +##### Requests to Delete + +California Users may request that we delete their personal information. Upon receiving a request from a User, we will take reasonable steps to verify the User’s identity. We will respond to a verified User request by: (a) deleting the User’s personal information and, if applicable, directing our service providers to delete the User’s personal information; or (ii) explaining to the User why the Act does not require us to delete their personal information. + +If a request or series of requests are manifestly unfounded or excessive, we may charge a reasonable fee for processing the request(s), or may refuse to process the request(s). + +##### Minors + +If we have actual knowledge that a User is under 16 years of age, we will not sell the User’s personal information unless the User – or, in the case of Users who are less than 13 years of age, the User’s parent or guardian – affirmatively “opts in” to the sale of the User’s information. + +### Personal Information We Have Collected, Sold, or Disclosed + +In the preceding 12 months, we have collected the following categories of personal information about California Users: + +- \*(A) Identifiers such as a real name, alias, postal address, unique personal identifier, online identifier, Internet Protocol address, email address, account name, social security number, driver’s license number, passport number, or other similar identifiers. +- \*\*(B) Any categories of personal information described in subdivision (e) of Section 1798.80. +- (C) Internet or other electronic network activity information, including, but not limited to information regarding a consumer’s interaction with an Internet Web site, application, or advertisement (specifically, our Services). +- (D) Professional or employment-related information. + +\*We have not sold any consumers’ personal information in the preceding 12 months. + +\*\*We have not shared for a business purpose any consumers’ personal information in the preceding 12 months. diff --git a/src/robots.txt b/src/robots.txt new file mode 100644 index 00000000..3c0dec27 --- /dev/null +++ b/src/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://share.unison-lang.org/sitemap.txt diff --git a/src/sitemap.txt b/src/sitemap.txt new file mode 100644 index 00000000..146609a6 --- /dev/null +++ b/src/sitemap.txt @@ -0,0 +1,2 @@ +https://share.unison-lang.org/@unison/base +https://share.unison-lang.org/@unison/cloud diff --git a/src/terms-of-service.md b/src/terms-of-service.md new file mode 100644 index 00000000..cc9e8b18 --- /dev/null +++ b/src/terms-of-service.md @@ -0,0 +1,149 @@ +These Terms of Service (this **"Agreement"**) set out the terms on which Unison Computing, PBC, a Delaware public benefit corporation with an address of 177 Huntington Ave, Ste 1703 PMB 30333 Boston, MA, 02115-3153 US (**"Unison"** **"we"** or **"us"**) will provide access to and use of Unison’s proprietary cloud computing platform (the **"Platform"**) and hosting service (the **"Hosting Service"**) (each an **"Application"**) through which you (**"you"** or **"User"**) can write, run and host programs in Unison’s cloud environment, utilizing features unique to the Unison programming language. The Applications individually and collectively are the **"Service"**. You should read this Agreement carefully. By indicating acceptance of this Agreement or by otherwise using the Service, you are entering into a legally binding agreement with us (and you hereby represent that you are of legal age in the jurisdiction from which you are accessing the Service, and are otherwise fully able and competent, to enter into a binding agreement). If you are using the Service on behalf of an organization, you represent that you have the right to bind such organization to this Agreement, and the terms "User" and "you" will include both you, the individual user, and such organization. We provide the use of the Service on the basis of this Agreement. If you do not agree to these terms and conditions, you must not use the Service. + +THIS AGREEMENT CREATES A BINDING LEGAL AGREEMENT BETWEEN YOU AND UNISON, AND INCLUDES AN ARBITRATION CLAUSE UNDER WHICH CERTAIN CLAIMS MAY NOT BE BROUGHT IN COURT OR DECIDED BY A JURY. PLEASE READ THIS AGREEMENT CAREFULLY. + +## 1. Nature of the Service + +Cloud Computing Platform and Hosting Service. The Service utilizes the Unison programming language to simplify cloud computing, through the Platform, which enables users to write and run programs, and the Hosting Service, which hosts users’ code in a cloud environment. Unison will use reasonable commercial efforts to provide the Service described in and subject to these Terms of Service. The Service may feature free versions of the Applications as well as paid premium versions with additional features. You may register to use the free or paid versions of one or both of the Applications through a single user account. + +## 2. Use of the Service + +### a. Eligibility + +In order to use the Service, you must register as a User. You are responsible for providing all of the other equipment necessary to access and use the Applications and the Service and for all related third-party charges (e.g. internet Service provider charges). You accept sole responsibility in accordance with this Agreement for, and Unison will not be responsible for, your use of the Applications or the Service. + +### b. Required Information + +To use the Service, you will be required to provide us with certain information including an email address and password, or to sign in using your Google or Github credentials. You represent and warrant to us that you will provide us with accurate, current and complete information. We reserve the right to refuse any requests to access and use the Service, without liability or justification. You are responsible for your registration, and for all use of the Service using any User credentials or passwords issued to you or chosen by you. You will keep all such credentials and passwords confidential. + +### c. Errors + +The Service may contain errors and inaccuracies for which we will not be liable to you or any other person, unless otherwise prohibited by law. We do not guarantee that the Service will be available to all individuals who desire to access and use the +Service. We reserve the right to limit access to the Service. + +### d. Compliance with Applicable Laws + +You represent and warrant that you comply and will at all times comply with all applicable laws and regulations in your use of the Service. + +## 3. Fees and Payment + +### a. Fees + +The basic versions of the Applications comprising the Service are offered free of charge to registered Users. Unison reserves the right to charge, and change eligibility requirements, for access to and use of some or all Applications, or the Service, at any time. Unison may offer premium versions of the Platform and the Hosting Service with paid subscription plans that allow users to access additional or exclusive features. If you purchase the premium version of one or both of the Applications, Unison will invoice you or charge your credit card for the subscription fee then in effect when you subscribe to the premium version(s). All fees are nonrefundable. You agree and represent that all information you provide to Unison for the purpose of subscribing to the Service is accurate, complete and current, and you agree to notify Unison of any changes to the credit card information associated with your Unison account, including changes in billing address and expiration dates. If Unison does not receive payment as due for a premium account, Unison reserves the right to either suspend or terminate such account and your access to the premium version in such circumstances. + +### b. Taxes + +All amounts due hereunder are exclusive of all sales, use, excise, service, value added, or other taxes, duties and charges of any kind, whether foreign, federal, state, local or other, associated with this Agreement, the Service, or your access to the Service. You shall be solely responsible for all such taxes, duties and charge, except for taxes imposed on Unison’s income, which may be invoiced by Unison from time to time. + +### c. Late Payments + +You shall pay interest on all late payments at the lesser of (a) 1.5% per month or (b) the highest rate permissible under applicable law, calculated daily and compounded monthly. You shall reimburse Unison for all costs and expenses, including attorneys’ fees, incurred in collecting any unpaid amounts owed by you hereunder. Additionally, in the event of late payment, Unison may in its sole discretion suspend User’s access to the premium version of the Application(s). + +### d. Usage Limits and True-Up + +You acknowledge that your use of the Service may be limited in volume, including as to Users. To the extent that the usage volume of any User of the premium version exceeds the volume covered by such User’s then-current subscription, Unison may invoice User an incremental or “true up” fee for such prior excess use at the rates, and User shall pay such invoice within thirty (30) days. For clarity, such invoice may issue after the expiration or termination of this Agreement. + +## 4. Intellectual Property Rights + +The Service is the property of, and owned by, Unison or its licensors. All the software, algorithms, functionality, inventions, designs, concepts, text, images, marks, logos, compilations, content and technology used to deliver the Service or otherwise embodied in, displayed through, or provided directly or indirectly (e.g., emails or other communications from us to you) via, the Service are the property of Unison or its licensors (**“Our Property”**). Except as otherwise expressly permitted by this Agreement, any use, copying, making derivative works, transmitting, posting, linking, deep linking, framing, redistribution, sale, decompilation, modification, reverse engineering, translation or disassembly of Our Property is prohibited. You consent to our obtaining injunctive relief to restrain any breach or threatened breach of this Agreement, without any requirement to post bond. + +The marks UNISON, UNISON SHARE, UNISON CLOUD, UNISON FORALL, and UNISON COMPUTING, and any associated logos, are registered or unregistered trademarks or service marks of Unison or its licensors. You may not use them, or any of our or their other marks or logos, in any manner, including any use that is likely to cause confusion or that disparages or discredits us, without our consent. The Service may also feature the trademarks, service marks, and logos of third parties, and each owner retains all rights in such marks. Any use of such marks, or any others displayed on the Service, will inure solely to the benefit of their respective owners. + +Subject to the terms and conditions herein, we grant you the non-exclusive, non-transferable, limited, revocable right to access and use Our Property solely as permitted by this Agreement. We reserve all other rights. For clarity and without limiting other obligations herein, Users shall not distribute or otherwise commercialize Our Property. + +### a. User Data + +Except as provided herein, you retain all rights and title in and to the information you provide to us when you register with us as a User of the Service. You hereby grant to Unison and its licensors the worldwide, royalty-free, irrevocable, perpetual, license to use, modify, reproduce, distribute, and create derivative works of all information and data you input into the Service as part of your registration or for administration of the Service (**“User Data”**), to provide the functionality of the Service and to further develop and improve the Service. Unison may use the User Data, together with data contributed by other Users to the Service, for research and analysis purposes. + +### b. Use of the Service + +You must comply with all rules and policies about use of the Service in this Agreement and that we publish from time to time. These rules and policies will be available on the Service. Certain features or content within the Service may contain supplemental terms of use, to which you must agree in order to use the relevant features or content. You must not: (a) harvest or otherwise collect information about others from the Service, except as expressly permitted through the functionality of the Service; (b) take any action that imposes or may impose an unreasonable or disproportionately large load on the Service or its infrastructure, or bypass any measures we may use to prevent or restrict access to any portion of the Service (or other accounts, networks or services connected thereto); (c) use manual or automated software, devices, or other processes to “crawl”, “scrape” or “spider” any of the Service or otherwise to copy, obtain, propagate, distribute or misappropriate any information or other content from the Service, including any of Our Property; (d) distribute or otherwise make available any information or other content obtained through the Service to any third party, except as expressly permitted herein; or (e) otherwise interfere in any manner with the use or operation of the Service. We reserve the right (but are under no obligation) to investigate any claim that use of the Service does not conform to the terms and conditions of this Agreement, and to terminate your use of the Service for breach of this Agreement. + +## 5. User Content + +You may upload or otherwise provide to the Service works of authorship, files, information, data and other content, including code (**“User Content”**). We do not claim any ownership rights in your User Content, and as between you and Unison, you remain the owner of all intellectual property rights that you have in your User Content. By using the Service, you grant Unison a worldwide, non-exclusive, royalty-free, perpetual, irrevocable, sub-licensable (through multiple tiers) right and license to use, reproduce, adapt, modify, translate, and create derivative works from your User Content, for the purposes of developing, providing, and improving the Service. You agree that we are not responsible for any use or disclosure of your User Content by any third party who gains access to it through the Service (which may include unintended activities by third parties, such as by hackers). To the extent allowed by law, this Section 5 and any license granted to you hereunder includes all rights of paternity, integrity, disclosure and withdrawal and any other rights that may be known as or referred to as “moral rights,” “creator’s rights,” “droit moral,” or the like. + +By submitting User Content, you represent and warrant that (i) you own or otherwise control all of the rights to your User Content and have all necessary rights to provide Unison with the User Content and the rights provided for herein (and you have all necessary rights from all persons and entities necessary to give you the rights to do the foregoing and otherwise fully comply with this Agreement), (ii) none of the User Content or any development, use, production, distribution or exploitation thereof hereunder will infringe, misappropriate or violate any intellectual property right or other right or applicable law, or cause injury to any person or entity, (iii) to the best of your knowledge, the User Content is truthful, accurate and not misleading, and (iv) you will comply with all applicable laws in the course of accessing, using and uploading or otherwise providing User Content to the Service. You agree that you bear all risks associated with your User Content. Unison does not permit the infringement of intellectual property rights on or through the Service, and will remove User Content from the Service if properly notified that such User Content infringes on another person’s or entity’s intellectual property rights. + +When submitting User Content to a Unison Share Project containing a license, you agree to license that User Content under those same terms, and represent and warrant that you have the right to license that User Content under those terms. If you have a separate agreement to license that User Content under different terms, such as a contributor license agreement, that agreement will supersede. + +We do not control User Content, and we are not responsible for its content, accuracy or reliability. We are under no obligation to edit or control User Content, although we reserve the right to review, and take certain actions with respect to, User Content in accordance with this Agreement, including the Privacy Practices described in Section 10. On termination of your account, or this Agreement, we have no obligation to return any User Content to you, so you should retain copies of all of Your Content. + +We reserve the right to remove User Content from the free version of the Service, in whole or in part, without prior notice, for any reason or for no reason at all. Without limiting our right to terminate a user pursuant to Section 11 of this Agreement, we reserve the right to terminate the account of any user of the Service who has been notified of infringing activity and/or has had User Content removed from the Service. We also reserve the right to decide whether User Content is appropriate and complies with this Agreement for violations other than violations of intellectual property law. For example, we may remove User Content which we in our sole discretion believe is untruthful, misleading, inaccurate, inflammatory, harmful, illegal or offensive. We may remove any User Content and/or terminate a User account for uploading material in violation of this Agreement at any time, without prior notice and at our sole discretion. + +## 6. Feedback + +If you provide to us (directly or indirectly, and by any means) any comments, feedback, suggestions, ideas, or other submissions related to the Service (collectively **“Feedback”**), the Feedback will be the sole property of Unison. We will be entitled to use, reproduce, disclose, publish, distribute, and otherwise exploit in any manner, all Feedback, without restriction and without compensating you in any way. We are and shall be under no obligation to maintain any Feedback in confidence, or to respond to any Feedback. + +## 7. Warranty Disclaimers and Limitations of Liability + +THE SERVICE IS PROVIDED “AS IS”, WITH ALL FAULTS. WE EXPRESSLY DISCLAIM ANY AND ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING: (A) ALL WARRANTIES RELATED TO THE SERVICE; (B) ALL WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NONINFRINGEMENT, AND ANY AND ALL WARRANTIES ARISING FROM COURSE OF DEALING OR USAGE OF TRADE; AND (B) THAT THE SERVICE, THE APPLICATIONS OR OUR PROPERTY WILL MEET YOUR REQUIREMENTS, WILL ALWAYS BE AVAILABLE, ACCESSIBLE, UNINTERRUPTED, TIMELY, SECURE OR OPERATE WITHOUT ERROR. WE DISCLAIM ALL LIABILITY AND RESPONSIBILITY FOR YOUR USE OF THE APPLICATIONS OR THE SERVICE, AND ANY EFFECTS OF THE APPLICATIONS OR THE SERVICE ON YOU OR ANY THIRD PARTY. We may pause or interrupt the Service at any time, and you should expect periodic downtime for updates to the Service. No advice or information, whether oral or written, obtained by you from us or through the Service will create any other warranty. + +UNDER NO CIRCUMSTANCES WILL YOU BE ENTITLED TO RECOVER FROM US ANY INCIDENTAL, CONSEQUENTIAL, INDIRECT, PUNITIVE OR SPECIAL DAMAGES (INCLUDING DAMAGES FOR LOSS OF DATA, OR LOSS OF USE), WHETHER BASED ON CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE ARISING FROM OR RELATING TO THIS AGREEMENT, THE SERVICE OR OUR PROPERTY, EVEN IF WE HAVE BEEN INFORMED OR SHOULD HAVE KNOWN OF THE POSSIBILITY OF SUCH DAMAGES. TO THE EXTENT PERMITTED BY APPLICABLE LAW, OUR MAXIMUM AGGREGATE LIABILITY TO YOU FOR ANY DAMAGES ARISING FROM OR RELATING TO THIS AGREEMENT, THE SERVICE, OR OUR PROPERTY, WHETHER BASED ON CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, SHALL BE LIMITED TO THE AMOUNTS PAID BY YOU TO US FOR THE SERVICE IN THE PRIOR YEAR (OR, IF YOU ARE A NON-FEE PAYING USER, TO THE AMOUNT OF $10). + +SOME JURISDICTIONS DO NOT ALLOW THE LIMITATION OR EXCLUSION OF WARRANTIES OR OF LIABILITY FOR CERTAIN TYPES OF DAMAGES, SO SOME OF THE ABOVE LIMITATIONS OR EXCLUSIONS MAY NOT APPLY TO YOU. + +## 8. Third Party Services + +Without limitation of the disclaimers and limitations of liability set forth in Section 8, you acknowledge and agree that Unison may provide the Service using third party licensors. Unison does not endorse, and hereby disclaims all liability or responsibility to you or any other person for, any third party Services. We reserve the right to change the terms of any third party relationship or terminate your access to the Service at any time upon notice to you due to a change necessitated by unforeseen circumstances that may arise after the date hereof, regulatory changes or changes imposed or required by a third party Service provider. + +## 9. Indemnity + +You will indemnify us, our affiliates, and our and their respective partners, members, trustees, directors, officers, employees, and licensors against any and all claims, actions, proceedings, suits, liabilities, losses, damages, costs, expenses and attorneys’ fees (**“Liabilities”**) arising out of or related to (a) your breach of this Agreement, or (b) your use of the Applications or Service (but excluding any Liabilities to the extent caused by our negligence or willful misconduct). We reserve the right to assume the sole control of the defense and settlement of any claim, action, suit or proceeding for which you are obliged to indemnify us. You will cooperate with us with respect to such defense and settlement. + +## 10. Our Privacy Practices + +We operate the Service under the Unison Privacy Policy published at [share.unison-lang.org/privacy-policy](https://share.unison-lang.org/privacy-policy) (the **“Privacy Policy”**), which is hereby incorporated into this Agreement. Each party shall comply with the Privacy Policy. + +## 11. Term and Termination. + +### a. Term + +The term of this Agreement (**“Term”**) shall commence on the Effective Date. The **“Effective Date”** shall be the date User first accesses the Service, for users of the free version, or the effective date of access to the premium version, for users of the premium version. If you purchase a paid subscription for the premium version of one or more Applications, the term of such subscription shall commence on the Effective Date for the paid subscription and, unless earlier terminated as set forth herein, shall continue for the subscription term you purchase, unless otherwise terminated as described in the next section, Section 11b. + +### b. Termination + +Either party may terminate this Agreement for convenience and without cause at any time by written notice thereof to the other party. + +If initiated by us, Unison will notify paid users of the effective date of termination. + +If initiated by the User: + +- For the premium version: Termination is effective as of the end of the current billing period and User retains access to the Service until the end of that period. +- For the free version, Termination is effective immediately. + +Any material breach of the Agreement by the User may result in a warning, limitation on their use of the Service, or immediate Termination of their account, at Unison's sole discretion. + +The User may also terminate this Agreement if Unison materially breaches this Agreement and does not cure such breach within fifteen (15) days after written notice thereof. Unison may terminate this Agreement immediately if User becomes the subject of any voluntary or involuntary petition in bankruptcy or any voluntary or involuntary proceeding relating to insolvency, receivership, liquidation, or composition for the benefit of creditors, if such petition or proceeding is not dismissed within sixty (60) days of filing. + +### c. Effects of Termination; Survival + +Upon any expiration or termination of this Agreement: (a) all rights granted to User hereunder shall terminate, and Unison shall no longer provide access to the Service to User, (b) User shall cease and cause its users to cease using the Service, and (c) each party shall promptly return or destroy any confidential information of the other party in its possession. Any obligations that have accrued prior to expiration or termination, including payment obligations, shall survive expiration or termination of this Agreement. In addition, the following Sections, as well as any other provisions herein which by their nature should survive, shall survive expiration or termination of this Agreement: 3d, 4-15, 17, 18. + +## 12. Modification of Service and Agreement + +We reserve the right to modify the free version of the Service at any time, without notice to you. We may also from time to time amend this Agreement prospectively. If we do so, we will notify you by posting on the Service. You agree that your continued use of the Service constitutes your agreement to the amended Agreement. If you do not agree to any amended Agreement that we publish, you must terminate your account and cease using the Service. Except as set forth above, this Agreement may be amended or modified only by an express writing signed by Unison. + +## 13. Children + +The Service is not directed to users under the age of 18. The Service does not knowingly collect personal information from children under the age of 13. + +## 14. Applicable Law + +You and we each agree that all disputes or other matters arising from or relating to this Agreement, or the use or operation of the Service, will be governed by the substantive laws of the Commonwealth of Massachusetts, U.S.A., without regard to its or any other jurisdiction’s conflicts of laws principles that would apply another law. Any action or proceeding by you relating to any claim arising from or relating to the Service or this Agreement must commence within the shorter of the applicable statute of limitations or one year after the cause of action has accrued. The United Nations Convention for the International Sale of Goods is hereby disclaimed. + +## 15. Arbitration + +We will attempt to resolve disputes with Users to their satisfaction. If, however, a matter arises that cannot be resolved promptly between you and us, you agree that any disputes arising out of or relating to the Service, the Applications or this Agreement (including the validity and scope of the agreement to arbitrate and any disputes with other users of the Service) shall be resolved exclusively by final and binding arbitration administered by the American Arbitration Association (“AAA”) under the Federal Arbitration Act, and shall be conducted before a single arbitrator pursuant to the applicable Rules and Procedures established by the AAA (for information on the AAA and its rules, see adr.org). You agree that the arbitration shall be held in Boston, Massachusetts unless the AAA or the arbitrator shall determine that venue in such city is unreasonably burdensome, in which case the AAA or the arbitrator shall select a venue that is not unreasonably burdensome to both you and us. You agree that, if the AAA shall be unavailable or decline to administer the arbitration, and the parties do not agree on a substitute, a substitute administrator or arbitrator shall be appointed by the court. The arbitrator may render early or summary disposition of some or all issues, after the parties have had a reasonable opportunity to make submissions on these issues. At Unison’s option, this provision shall not apply to claims of patent, trademark, or copyright infringement or misappropriation of trade secrets (collectively, “IP Claims”). With respect to any IP Claims that are not subject to arbitration under the above provision, you hereby consent to non-exclusive jurisdiction and venue in any federal or state court located within Boston, Massachusetts, U.S.A., with respect to any suit, claim or cause of action arising from or relating to the Service or this Agreement, and you shall not bring any such suit, claim or cause of action except in a court located within Boston, Massachusetts, U.S.A. You agree that any arbitration shall not permit claims on a class, mass, representative, or private attorney general basis. You further agree that no claims of other parties may be consolidated with your or our claims in the arbitration without both your and our consent. YOU ARE WAIVING YOUR RIGHTS TO HAVE YOUR CASE DECIDED BY A JURY AND TO PARTICIPATE IN A CLASS, MASS, REPRESENTATIVE, PRIVATE ATTORNEY GENERAL, OR CONSOLIDATED ACTION AGAINST US. If any part of this Arbitration clause is later deemed invalid as a matter of law, then it shall be severed and the remaining portions of this section shall remain in effect, with the exception that if the preceding paragraph is deemed invalid, then this entire section shall be deemed invalid and the arbitration clause shall be void. + +## 16. Force Majeure + +Unison will not be liable for delay or non-performance of any of its obligations hereunder or its performance of the Service to the extent that such performance is prevented, prohibited or delayed, or such loss or destruction is caused, by any circumstance for reasons beyond its control including without limitation, labor disputes, fire, flood, natural disaster, war blockade, military operations, riot, epidemic, civil commotion, plant breakdown, power outage, local, state or national state of emergency, computer or other equipment failure or non-delivery or delays in delivery by any other suppliers of goods or services utilized in the performance of the Service under this Agreement. + +## 17. Geography + +We provide the Service from the United States. We make no claims that the Service is accessible or appropriate in all locations inside the United States or outside of the United States. Access to the Service may not be legal by certain persons or in certain countries. If you access the Service from outside the United States, you do so on your own initiative and are responsible for compliance with local laws. + +## 18. Miscellaneous Provision + +No delay or omission by us in exercising any of our rights occurring upon any noncompliance or default by you with respect to any of the terms and conditions of this Agreement will impair any such right or be construed to be a waiver thereof, and a waiver by us of any of the covenants, conditions or agreements to be performed by you will not be construed to be a waiver of any succeeding breach thereof or of any other covenant, condition or agreement herein. No waiver will be binding on us unless made in an express writing signed by us. If any provision of this Agreement is found by a court of competent jurisdiction to be invalid or unenforceable, then this Agreement will remain in full force and effect and will be reformed to be valid and enforceable while reflecting the intent of the parties to the greatest extent permitted by law. Except as otherwise expressly provided herein, this Agreement sets forth the entire agreement between us and you regarding its subject matter, and supersedes all prior promises, agreements or representations, whether written or oral, regarding such subject matter. No pre-printed terms on any purchase order, invoice or similar document issued in relation to this Agreement shall have any effect on the parties or this Agreement. Neither party may assign or otherwise transfer this Agreement, or assign or otherwise transfer any of its rights hereunder, or delegate any of its obligations hereunder, without the prior written consent of the other party; provided, Unison may assign or otherwise transfer this Agreement, or assign or otherwise transfer any of its rights or delegate any of its obligations hereunder to an affiliate or to a successor to all or substantially all of its assets, stock or business, without your prior written consent. Any purported assignment or delegation in violation of this paragraph is null and void. This Agreement will bind and inure to the benefit of each party’s successors and permitted assigns. You agree that the electronic text of this Agreement constitutes a writing and your assent to the terms and conditions hereof constitutes a “signing” for all purposes. As used herein and unless the intent is expressly otherwise in a specific instance, the terms “include,” “includes” or “including” shall not be limiting and “or” shall not be exclusive. Any section headings herein are for convenience only and do not form a part of, and will not be used in the interpretation of, the substantive provisions of this Agreement. You agree that email to your email address on record will constitute formal notice under this Agreement. Any notice to Unison shall be deemed to have been received as follows: (a) by personal delivery, upon receipt; (b) by guaranteed overnight delivery, one business day after transmission or dispatch; or (c) by certified mail, as evidenced by the return receipt. Notices to Unison may be sent to Unison’s address set forth in the preamble to this Agreement. diff --git a/src/unisonShare.ejs b/src/unisonShare.ejs new file mode 100644 index 00000000..ac5592ed --- /dev/null +++ b/src/unisonShare.ejs @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + Unison Share + + + + + diff --git a/src/unisonShare.js b/src/unisonShare.js new file mode 100644 index 00000000..48a12c4d --- /dev/null +++ b/src/unisonShare.js @@ -0,0 +1,113 @@ +import "ui-core/css/ui.css"; +import "ui-core/css/themes/unison-light.css"; +import "ui-core/css/code.css"; +// Include web components +import "ui-core/UI/CopyOnClick"; +import "ui-core/UI/ModalOverlay"; +import "ui-core/UI/CopyrightYear"; +import "ui-core/Lib/OnClickOutside"; +import "ui-core/Lib/EmbedKatex"; +import "ui-core/Lib/MermaidDiagram"; +import "ui-core/Lib/EmbedSvg"; +import detectOs from "ui-core/Lib/detectOs"; +import preventDefaultGlobalKeyboardEvents from "ui-core/Lib/preventDefaultGlobalKeyboardEvents"; +import * as Sentry from "@sentry/browser"; +import { BrowserTracing } from "@sentry/tracing"; + +import "./UnisonShare/SupportChatWidget"; +import { getCookie } from "./util"; +import * as Metrics from "./metrics"; + +import "./css/unison-share.css"; +import { Elm } from "./UnisonShare.elm"; + +console.log(` + _____ _ +| | |___|_|___ ___ ___ +| | | | |_ -| . | | +|_____|_|_|_|___|___|_|_| + + +`); + +// ---------------------------------------------------------------------------- + +preventDefaultGlobalKeyboardEvents(); + +Metrics.init(); + +if (APP_ENV === "production") { + Sentry.init({ + dsn: "https://8eb2ee6bb78d4131bdbb1b6a70f6b0c0@o4503934538547200.ingest.sentry.io/4504458036903936", + integrations: [new BrowserTracing()], + sampleRate: 0.25, + environment: APP_ENV, + }); +} + +// ---------------------------------------------------------------------------- + +const basePath = new URL(document.baseURI).pathname; + +const Storage = { + backupStorage: new Map(), + + mode() { + try { + localStorage.length; + return "localStorage"; + } catch (e) { + try { + sessionStorage.length; + return "sessionStorage"; + } catch (e) { + return "backupStorage"; + } + } + }, + + set(key, val) { + const mode = Storage.mode(); + if (mode === "backupStorage") { + Storage.backupStorage.set(key, val); + } else { + window[mode].setItem(key, val); + } + }, + + get(key) { + const mode = Storage.mode(); + if (mode === "backupStorage") { + return Storage.backupStorage.get(key) || null; + } else { + return window[mode].getItem(key); + } + }, +}; + +const whatsNewReadPostIds = + JSON.parse(Storage.get("whatsNewReadPostIds")) || []; + +const flags = { + operatingSystem: detectOs(window.navigator), + basePath, + apiUrl: API_URL, + websiteUrl: WEBSITE_URL, + xsrfToken: getCookie("XSRF-TOKEN"), + appEnv: APP_ENV, + whatsNewReadPostIds, +}; + +// The main entry point for the `UnisonShare` target of the Codebase UI. +const app = Elm.UnisonShare.init({ flags }); + +// Ports can be dead code eliminated, so we have to check if they exist +if (app.ports) { + app.ports.trackEvent?.subscribe((eventName) => { + Metrics.track(eventName); + }); + + app.ports.updateWhatsNewReadPostIds?.subscribe((postIds) => { + Storage.set("whatsNewReadPostIds", JSON.stringify(postIds)); + }); +} diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000..fb1e8fa3 --- /dev/null +++ b/src/util.js @@ -0,0 +1,12 @@ +function getCookie(name) { + if (navigator.cookieEnabled) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(";").shift(); + else return null; + } else { + return null; + } +} + +export { getCookie }; diff --git a/tests/UnisonShare/Contribution/ContributionRefTests.elm b/tests/UnisonShare/Contribution/ContributionRefTests.elm new file mode 100644 index 00000000..a716fb7f --- /dev/null +++ b/tests/UnisonShare/Contribution/ContributionRefTests.elm @@ -0,0 +1,59 @@ +module UnisonShare.Contribution.ContributionRefTests exposing (..) + +import Expect +import Test exposing (..) +import UnisonShare.Contribution.ContributionRef as ContributionRef + + +fromInt : Test +fromInt = + describe "ContributionRef.fromInt" + [ test "parse an int into a ContributionRef" <| + \_ -> + Expect.equal + (2023 + |> ContributionRef.fromInt + |> Maybe.map ContributionRef.toString + |> Maybe.withDefault "FAIL!" + ) + "#2023" + , test "fails to parse int of 0" <| + \_ -> + Expect.equal + (ContributionRef.fromInt 0) + Nothing + , test "fails to parse a negative int" <| + \_ -> + Expect.equal + (ContributionRef.fromInt -9) + Nothing + ] + + +fromString : Test +fromString = + describe "ContributionRef.fromString" + [ test "parse a string into a ContributionRef" <| + \_ -> + Expect.equal + ("2023" + |> ContributionRef.fromString + |> Maybe.map ContributionRef.toString + |> Maybe.withDefault "FAIL!" + ) + "#2023" + , test "fails to parse an invalid string" <| + \_ -> + Expect.equal + (ContributionRef.fromString "invalid") + Nothing + ] + + +toString : Test +toString = + describe "ContributionRef.toString" + [ test "render the ref as a string" <| + \_ -> + Expect.equal "#2023" (ContributionRef.toString (ContributionRef.unsafeFromString "2023")) + ] diff --git a/tests/UnisonShare/Project/ProjectRefTests.elm b/tests/UnisonShare/Project/ProjectRefTests.elm new file mode 100644 index 00000000..45f69794 --- /dev/null +++ b/tests/UnisonShare/Project/ProjectRefTests.elm @@ -0,0 +1,85 @@ +module UnisonShare.Project.ProjectRefTests exposing (..) + +import Code.ProjectSlug as ProjectSlug +import Expect +import Lib.UserHandle as UserHandle +import Test exposing (..) +import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) + + +toString : Test +toString = + describe "ProjectRef.toString" + [ test "Formats the ref to a string" <| + \_ -> + Expect.equal + "@unison/http" + (ProjectRef.toString projectRef) + ] + + +fromString : Test +fromString = + describe "ProjectRef.fromString" + [ test "creates a ProjectRef from valid handle string and a valid slug string" <| + \_ -> + let + result = + ProjectRef.fromString "@unison" "http" + |> Maybe.map ProjectRef.toString + |> Maybe.withDefault "FAIL" + in + Expect.equal "@unison/http" result + ] + + +unsafeFromString : Test +unsafeFromString = + describe "ProjectRef.unsafeFromString" + [ test "creates a ProjectRef from valid unprefixed handle string and a valid slug string" <| + \_ -> + let + result = + ProjectRef.unsafeFromString "unison" "http" + |> ProjectRef.toString + in + Expect.equal "@unison/http" result + ] + + +handle : Test +handle = + describe "ProjectRef.handle" + [ test "Extracts the handle" <| + \_ -> + Expect.equal + "@unison" + (ProjectRef.handle projectRef |> UserHandle.toString) + ] + + +slug : Test +slug = + describe "ProjectRef.slug" + [ test "Extracts the slug" <| + \_ -> + Expect.equal + "http" + (ProjectRef.slug projectRef |> ProjectSlug.toString) + ] + + + +-- Helpers + + +projectRef : ProjectRef +projectRef = + let + handle_ = + UserHandle.unsafeFromString "unison" + + slug_ = + ProjectSlug.unsafeFromString "http" + in + ProjectRef.projectRef handle_ slug_ diff --git a/tests/UnisonShare/Project/ReleaseDownloadsTest.elm b/tests/UnisonShare/Project/ReleaseDownloadsTest.elm new file mode 100644 index 00000000..a293382c --- /dev/null +++ b/tests/UnisonShare/Project/ReleaseDownloadsTest.elm @@ -0,0 +1,56 @@ +module UnisonShare.Project.ReleaseDownloadsTest exposing (..) + +import Expect +import Test exposing (..) +import UnisonShare.Project.ReleaseDownloads as ReleaseDownloads exposing (ReleaseDownloads(..)) + + +weeklyAverage : Test +weeklyAverage = + describe "ReleaseDownloads.weeklyAverage" + [ test "calculates the average of the last 4 weeks (avg of avg)" <| + \_ -> + Expect.equal + 9 + (ReleaseDownloads.weeklyAverage releaseDownloads_) + ] + + +releaseDownloads_ : ReleaseDownloads +releaseDownloads_ = + ReleaseDownloads + [ 3 + , 4 + , 6 + , 8 + , 22 + , 11 + , 3 + + -- + , 6 + , 4 + , 8 + , 13 + , 16 + , 10 + , 9 + + -- + , 7 + , 7 + , 9 + , 14 + , 24 + , 10 + , 7 + + -- + , 14 + , 8 + , 9 + , 4 + , 2 + , 18 + , 4 + ] diff --git a/tests/UnisonShare/ProjectTests.elm b/tests/UnisonShare/ProjectTests.elm new file mode 100644 index 00000000..77ddc099 --- /dev/null +++ b/tests/UnisonShare/ProjectTests.elm @@ -0,0 +1,106 @@ +module UnisonShare.ProjectTests exposing (..) + +import Code.ProjectSlug as ProjectSlug +import Expect +import Lib.UserHandle as UserHandle +import Set +import Test exposing (..) +import Time +import UI.DateTime as DateTime +import UnisonShare.Project as Project exposing (Project) +import UnisonShare.Project.ProjectRef as ProjectRef +import UnisonShare.Project.ReleaseDownloads exposing (ReleaseDownloads(..)) + + +ref : Test +ref = + describe "Project.ref" + [ test "Returns the ref of a project by handle and slug" <| + \_ -> + Expect.equal + "@unison/http" + (Project.ref project |> ProjectRef.toString) + ] + + +handle : Test +handle = + describe "Project.handle" + [ test "Returns the handle of a project" <| + \_ -> + Expect.equal + "@unison" + (Project.handle project |> UserHandle.toString) + ] + + +slug : Test +slug = + describe "Project.slug" + [ test "Returns the slug of a project" <| + \_ -> + Expect.equal + "http" + (Project.slug project |> ProjectSlug.toString) + ] + + +toggleFav : Test +toggleFav = + let + resultFor isFaved = + let + p = + Project.toggleFav (projectDetails isFaved 3) + in + ( p.isFaved, p.numFavs ) + in + describe "Project.toggleFav" + [ test "toggles Unknown to Unknown" <| + \_ -> + Expect.equal (resultFor Project.Unknown) ( Project.Unknown, 3 ) + , test "toggles Faved to NotFaved" <| + \_ -> + Expect.equal (resultFor Project.Faved) ( Project.NotFaved, 2 ) + , test "toggles JustFaved to NotFaved" <| + \_ -> + Expect.equal (resultFor Project.JustFaved) ( Project.NotFaved, 2 ) + , test "toggles NotFaved to JustFaved" <| + \_ -> + Expect.equal (resultFor Project.NotFaved) ( Project.JustFaved, 4 ) + ] + + + +-- Helpers + + +project : Project {} +project = + { ref = + ProjectRef.projectRef + (UserHandle.unsafeFromString "unison") + (ProjectSlug.unsafeFromString "http") + , visibility = Project.Public + } + + +projectDetails : Project.IsFaved -> Int -> Project.ProjectDetails +projectDetails isFaved numFavs = + { ref = + ProjectRef.projectRef + (UserHandle.unsafeFromString "unison") + (ProjectSlug.unsafeFromString "http") + , isFaved = isFaved + , numFavs = numFavs + , numActiveContributions = 0 + , numOpenTickets = 0 + , releaseDownloads = ReleaseDownloads [] + , summary = Just "hi i'm a summary" + , tags = Set.empty + , visibility = Project.Public + , latestVersion = Nothing + , defaultBranch = Nothing + , createdAt = DateTime.fromPosix (Time.millisToPosix 1) + , updatedAt = DateTime.fromPosix (Time.millisToPosix 1) + } diff --git a/tests/UnisonShare/RouteTests.elm b/tests/UnisonShare/RouteTests.elm new file mode 100644 index 00000000..66f41daa --- /dev/null +++ b/tests/UnisonShare/RouteTests.elm @@ -0,0 +1,453 @@ +module UnisonShare.RouteTests exposing (..) + +import Code.BranchRef as BranchRef +import Code.Definition.Reference as Reference exposing (Reference(..)) +import Code.FullyQualifiedName as FQN exposing (FQN) +import Code.Perspective as Perspective +import Code.Version as Version +import Expect +import Lib.UserHandle as UserHandle +import Test exposing (..) +import UI.ViewMode as ViewMode +import UnisonShare.AppError as AppError +import UnisonShare.Project.ProjectRef as ProjectRef +import UnisonShare.Route as Route exposing (CodeRoute(..), ProjectRoute(..), Route(..), UserRoute(..)) +import Url exposing (Url) + + +catalogRoute : Test +catalogRoute = + describe "Route.fromUrl : catalog route" + [ test "Matches /catalog to Catalog" <| + \_ -> + let + url = + mkUrl "/catalog" + in + Expect.equal Catalog (Route.fromUrl "" url) + , test "Matches root to Catalog" <| + \_ -> + let + url = + mkUrl "/" + in + Expect.equal Catalog (Route.fromUrl "" url) + ] + + +accountRoute : Test +accountRoute = + describe "Route.fromUrl : account route" + [ test "Matches /account to Account" <| + \_ -> + let + url = + mkUrl "/account" + in + Expect.equal Account (Route.fromUrl "" url) + ] + + +termsOfServiceRoute : Test +termsOfServiceRoute = + describe "Route.fromUrl : terms of service route" + [ test "Matches /terms-of-service to TermsOfService" <| + \_ -> + let + url = + mkUrl "/terms-of-service" + in + Expect.equal TermsOfService (Route.fromUrl "" url) + ] + + +privacyPolicyRoute : Test +privacyPolicyRoute = + describe "Route.fromUrl : privacy policy route" + [ test "Matches /privacy-policy to PrivacyPolicy" <| + \_ -> + let + url = + mkUrl "/privacy-policy" + in + Expect.equal PrivacyPolicy (Route.fromUrl "" url) + ] + + +ucmConnectedRoute : Test +ucmConnectedRoute = + describe "Route.fromUrl : ucm connected route" + [ test "Matches /ucm-connected to UcmConnected" <| + \_ -> + let + url = + mkUrl "/ucm-connected" + in + Expect.equal UcmConnected (Route.fromUrl "" url) + ] + + +cloudRoute : Test +cloudRoute = + describe "Route.fromUrl : cloud route" + [ test "Matches /cloud to Cloud" <| + \_ -> + let + url = + mkUrl "/cloud" + in + Expect.equal Cloud (Route.fromUrl "" url) + ] + + +error : Test +error = + describe "Route.fromUrl : error route" + [ test "Matches /error?appError=UnspecifiedError to Error" <| + \_ -> + let + url = + mkUrl "/error?appError=UnspecifiedError" + in + Expect.equal (Error AppError.UnspecifiedError) (Route.fromUrl "" url) + ] + + +notFound : Test +notFound = + describe "Route.fromUrl : not found route" + [ test "Matches /this-does-not-exist to NotFound" <| + \_ -> + let + url = + mkUrl "/this-does-not-exist" + in + Expect.equal (NotFound "/this-does-not-exist") (Route.fromUrl "" url) + ] + + + +-- USERS ROUTE + + +userProfileRoute : Test +userProfileRoute = + describe "Route.fromUrl : user profile route" + [ test "Matches /:handle to UserProfile " <| + \_ -> + let + url = + mkUrl "/@unison" + in + Expect.equal + (User (UserHandle.unsafeFromString "unison") UserProfile) + (Route.fromUrl "" url) + ] + + +userContributionsRoute : Test +userContributionsRoute = + describe "Route.fromUrl : user contributions route" + [ test "Matches /:handle/p/contributions to UserContributions " <| + \_ -> + let + url = + mkUrl "/@unison/p/contributions" + in + Expect.equal + (User (UserHandle.unsafeFromString "unison") UserContributions) + (Route.fromUrl "" url) + ] + + +userCodeRoute : Test +userCodeRoute = + describe "Route.fromUrl : user code route" + [ test "Matches /:handle/p/code/latest to UserCode" <| + \_ -> + let + url = + mkUrl "/@unison/p/code/latest" + in + Expect.equal + (User (UserHandle.unsafeFromString "unison") + (UserCode + ViewMode.Regular + (CodeRoot (Perspective.toParams Perspective.relativeRootPerspective)) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/p/code/latest/namespaces/data/List to UserCode with a perspective" <| + \_ -> + let + url = + mkUrl "/@unison/p/code/latest/namespaces/data/List" + in + Expect.equal + (User (UserHandle.unsafeFromString "unison") + (UserCode + ViewMode.Regular + (CodeRoot (Perspective.toParams (Perspective.namespacePerspective (fqn "data.List")))) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/p/code/latest/terms/data/List/map to UserCode with a definition" <| + \_ -> + let + url = + mkUrl "/@unison/p/code/latest/terms/data/List/map" + in + Expect.equal + (User (UserHandle.unsafeFromString "unison") + (UserCode + ViewMode.Regular + (Definition (Perspective.toParams Perspective.relativeRootPerspective) (termRef "data.List.map")) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/p/code/latest/namespaces/data/List/;/terms/map to UserCode with a perspective and definition" <| + \_ -> + let + url = + mkUrl "/@unison/p/code/latest/namespaces/data/List/;/terms/map" + in + Expect.equal + (User (UserHandle.unsafeFromString "unison") + (UserCode + ViewMode.Regular + (Definition + (Perspective.toParams (Perspective.namespacePerspective (fqn "data.List"))) + (termRef "map") + ) + ) + ) + (Route.fromUrl "" url) + ] + + + +-- PROJECTS ROUTE + + +projectOverviewRoute : Test +projectOverviewRoute = + describe "Route.fromUrl : project overview route" + [ test "Matches /:handle/:slug to ProjectOverview " <| + \_ -> + let + url = + mkUrl "/@unison/base" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + ProjectOverview + ) + (Route.fromUrl "" url) + ] + + +projectBranchesRoute : Test +projectBranchesRoute = + describe "Route.fromUrl : project branches route" + [ test "Matches /:handle/:slug/branches to ProjectBranches " <| + \_ -> + let + url = + mkUrl "/@unison/base/branches" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + ProjectBranches + ) + (Route.fromUrl "" url) + ] + + +projectBranchRoute : Test +projectBranchRoute = + describe "Route.fromUrl : project branch route" + [ test "Matches /:handle/:slug/code/:branchRef to ProjectBranch" <| + \_ -> + let + url = + mkUrl "/@unison/base/code/main" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectBranch + (BranchRef.unsafeFromString "main") + ViewMode.Regular + (CodeRoot (Perspective.toParams Perspective.relativeRootPerspective)) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/:slug/code/@user/branch to ProjectBranch with a contributor branch" <| + \_ -> + let + url = + mkUrl "/@unison/base/code/@user/branch" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectBranch + (BranchRef.unsafeFromString "user/branch") + ViewMode.Regular + (CodeRoot (Perspective.toParams Perspective.relativeRootPerspective)) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/:slug/code/releases/drafts/1.2.3 to ProjectBranch with a release draft branch" <| + \_ -> + let + url = + mkUrl "/@unison/base/code/releases/drafts/1.2.3" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectBranch + (BranchRef.unsafeFromString "releases/drafts/1.2.3") + ViewMode.Regular + (CodeRoot (Perspective.toParams Perspective.relativeRootPerspective)) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/:slug/code/releases/1.2.3 to ProjectBranch with a release branch" <| + \_ -> + let + url = + mkUrl "/@unison/base/code/releases/1.2.3" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectBranch + (BranchRef.unsafeFromString "releases/1.2.3") + ViewMode.Regular + (CodeRoot (Perspective.toParams Perspective.relativeRootPerspective)) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/:slug/code/:branchRef/latest/namespaces/data/List to ProjectBranch with a perspective" <| + \_ -> + let + url = + mkUrl "/@unison/base/code/main/latest/namespaces/data/List" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectBranch + (BranchRef.unsafeFromString "main") + ViewMode.Regular + (CodeRoot (Perspective.toParams (Perspective.namespacePerspective (fqn "data.List")))) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/:slug/code/:branchRef/latest/terms/data/List/map to ProjectBranch with a definition" <| + \_ -> + let + url = + mkUrl "/@unison/base/code/main/latest/terms/data/List/map" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectBranch + (BranchRef.unsafeFromString "main") + ViewMode.Regular + (Definition (Perspective.toParams Perspective.relativeRootPerspective) (termRef "data.List.map")) + ) + ) + (Route.fromUrl "" url) + , test "Matches /:handle/:slug/code/:branchRef/latest/namespaces/data/List/;/terms/map to ProjectBranch with a perspective and definition" <| + \_ -> + let + url = + mkUrl "/@unison/base/code/main/latest/namespaces/data/List/;/terms/map" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectBranch + (BranchRef.unsafeFromString "main") + ViewMode.Regular + (Definition + (Perspective.toParams (Perspective.namespacePerspective (fqn "data.List"))) + (termRef "map") + ) + ) + ) + (Route.fromUrl "" url) + ] + + +projectReleasesRoute : Test +projectReleasesRoute = + describe "Route.fromUrl : project releases route" + [ test "Matches /:handle/:slug/releases to ProjectReleases" <| + \_ -> + let + url = + mkUrl "/@unison/base/releases" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + ProjectReleases + ) + (Route.fromUrl "" url) + ] + + +projectReleaseRoute : Test +projectReleaseRoute = + describe "Route.fromUrl : project release route" + [ test "Matches /:handle/:slug/releases/:version to ProjectRelease" <| + \_ -> + let + url = + mkUrl "/@unison/base/releases/1.2.3" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + (ProjectRelease (Version.version 1 2 3)) + ) + (Route.fromUrl "" url) + ] + + +projectSettingsRoute : Test +projectSettingsRoute = + describe "Route.fromUrl : project settings route" + [ test "Matches /:handle/:slug/settings to ProjectSettings" <| + \_ -> + let + url = + mkUrl "/@unison/base/settings" + in + Expect.equal + (Project (ProjectRef.unsafeFromString "unison" "base") + ProjectSettings + ) + (Route.fromUrl "" url) + ] + + + +-- HELPERS + + +fqn : String -> FQN +fqn = + FQN.fromString + + +termRef : String -> Reference +termRef str = + Reference.fromString Reference.TermReference str + + +mkUrl : String -> Url +mkUrl path = + { protocol = Url.Https + , host = "unison-lang.org" + , port_ = Just 443 + , path = path + , query = Nothing + , fragment = Nothing + } diff --git a/tests/UnisonShare/Ticket/TicketRefTests.elm b/tests/UnisonShare/Ticket/TicketRefTests.elm new file mode 100644 index 00000000..fea90e8b --- /dev/null +++ b/tests/UnisonShare/Ticket/TicketRefTests.elm @@ -0,0 +1,59 @@ +module UnisonShare.Ticket.TicketRefTests exposing (..) + +import Expect +import Test exposing (..) +import UnisonShare.Ticket.TicketRef as TicketRef + + +fromInt : Test +fromInt = + describe "TicketRef.fromInt" + [ test "parse an int into a TicketRef" <| + \_ -> + Expect.equal + (2023 + |> TicketRef.fromInt + |> Maybe.map TicketRef.toString + |> Maybe.withDefault "FAIL!" + ) + "#2023" + , test "fails to parse int of 0" <| + \_ -> + Expect.equal + (TicketRef.fromInt 0) + Nothing + , test "fails to parse a negative int" <| + \_ -> + Expect.equal + (TicketRef.fromInt -9) + Nothing + ] + + +fromString : Test +fromString = + describe "TicketRef.fromString" + [ test "parse a string into a TicketRef" <| + \_ -> + Expect.equal + ("2023" + |> TicketRef.fromString + |> Maybe.map TicketRef.toString + |> Maybe.withDefault "FAIL!" + ) + "#2023" + , test "fails to parse an invalid string" <| + \_ -> + Expect.equal + (TicketRef.fromString "invalid") + Nothing + ] + + +toString : Test +toString = + describe "TicketRef.toString" + [ test "render the ref as a string" <| + \_ -> + Expect.equal "#2023" (TicketRef.toString (TicketRef.unsafeFromString "2023")) + ] diff --git a/tests/e2e/catalog.spec.ts b/tests/e2e/catalog.spec.ts new file mode 100644 index 00000000..2d61f6db --- /dev/null +++ b/tests/e2e/catalog.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from "@playwright/test"; + +const TIMEOUT = 500; + +test.describe("Catalog", () => { + test("Simulate the Checkly check", async ({ page }) => { + await page.route("*/**/api/account", async (route) => { + const json = { + avatarUrl: null, + completedTours: ["welcome-terms"], + handle: "testuser", + name: "Test User", + organizationMemberships: [], + primaryEmail: "test@example.com", + userId: "testuserid", + }; + + await route.fulfill({ json }); + }); + + await page.route("*/**/api/catalog", async (route) => { + const json = [ + { + name: "Featured", + projects: [ + { + createdAt: "2023-05-25T01:39:01.955533Z", + isFaved: true, + numFavs: 4, + owner: { + handle: "@unison", + name: "Unison", + type: "organization", + }, + slug: "base", + summary: "The unison base library.", + tags: [], + updatedAt: "2023-06-05T02:37:25.346367Z", + visibility: "public", + }, + { + createdAt: "2023-04-03T17:05:08.873717Z", + isFaved: false, + numFavs: 5, + owner: { + handle: "@unison", + name: "Unison", + type: "organization", + }, + slug: "distributed", + summary: + "A library for distributed computing. Computations can be run locally or on unison.cloud.", + tags: [], + updatedAt: "2023-04-05T02:38:23.774294Z", + visibility: "public", + }, + ], + }, + ]; + + await route.fulfill({ json }); + }); + + const response = await page.goto("http://localhost:1234/"); + // Test that the response did not fail + expect(response?.status()).toBeLessThan(400); + + const base = page.getByTitle("@unison/base"); + await expect(base).toHaveClass("project-name", { timeout: TIMEOUT }); + const distributed = page.getByTitle("@unison/distributed"); + await expect(distributed).toHaveClass("project-name", { timeout: TIMEOUT }); + }); +}); diff --git a/webpack.dev.js b/webpack.dev.js new file mode 100644 index 00000000..6b4cea73 --- /dev/null +++ b/webpack.dev.js @@ -0,0 +1,133 @@ +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const webpack = require("webpack"); +const postcssPresetEnv = require("postcss-preset-env"); +const FaviconsWebpackPlugin = require("favicons-webpack-plugin"); + +const API_URL = process.env.API_URL || "http://127.0.0.1:5424"; +const UI_CORE_SRC = "elm-stuff/gitdeps/github.com/unisonweb/ui-core/src"; +const WEBSITE_URL = process.env.WEBSITE_URL || "https://www.unison-lang.org"; + +module.exports = { + entry: "./src/unisonShare.js", + + resolve: { + alias: { + assets: path.resolve(__dirname, "src/assets/"), + "ui-core": path.resolve(__dirname, UI_CORE_SRC + "/"), + }, + }, + + module: { + rules: [ + { + test: /\.css$/i, + use: [ + "style-loader", + { + loader: "css-loader", + options: { importLoaders: 1 }, + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [ + postcssPresetEnv({ + features: { + "is-pseudo-class": false, + "custom-media-queries": { + importFrom: `${UI_CORE_SRC}/css/ui/viewport.css`, + }, + }, + }), + ], + }, + }, + }, + ], + }, + { + test: /\.md$/i, + type: "asset/source", + }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: "asset/resource", + }, + { + test: /\.(woff(2)?|ttf|eot)$/i, + type: "asset/resource", + }, + { + test: /\.elm$/, + exclude: [/elm-stuff/, /node_modules/], + use: [ + { + loader: "elm-asset-webpack-loader", + }, + { + loader: "elm-webpack-loader", + options: { + debug: false, + cwd: __dirname, + }, + }, + ], + }, + ], + }, + + plugins: [ + new HtmlWebpackPlugin({ + template: "./src/unisonShare.ejs", + inject: "body", + publicPath: "/", + base: "/", + filename: path.resolve(__dirname, "dist/dev/index.html"), + }), + + new FaviconsWebpackPlugin({ + logo: "./src/assets/dev-favicon.svg", + inject: true, + favicons: { + appName: "Unison Share", + appDescription: "Explore, read docs about, and share Unison libraries", + developerName: "Unison", + developerURL: "https://unison-lang.org", + background: "#C6A8EC", + theme_color: "#C6A8EC", + }, + }), + + new webpack.DefinePlugin({ + API_URL: JSON.stringify("api"), + WEBSITE_URL: JSON.stringify("website"), + APP_ENV: JSON.stringify("development"), + }), + ], + + output: { + filename: "[name].[contenthash].js", + path: path.resolve(__dirname, "dist/dev"), + clean: true, + }, + + devServer: { + historyApiFallback: { + disableDotRule: true, + }, + proxy: { + "/api": { + target: API_URL, + pathRewrite: { "^/api": "" }, + logLevel: "debug", + }, + "/website": { + target: WEBSITE_URL, + pathRewrite: { "^/website": "" }, + logLevel: "debug", + }, + }, + }, +}; diff --git a/webpack.prod.js b/webpack.prod.js new file mode 100644 index 00000000..71bf2c2c --- /dev/null +++ b/webpack.prod.js @@ -0,0 +1,158 @@ +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const CopyPlugin = require("copy-webpack-plugin"); +const FaviconsWebpackPlugin = require("favicons-webpack-plugin"); +const webpack = require("webpack"); +const postcssPresetEnv = require("postcss-preset-env"); + +const API_URL = process.env.API_URL || "https://api.unison-lang.org"; +const UI_CORE_SRC = "elm-stuff/gitdeps/github.com/unisonweb/ui-core/src"; +const WEBSITE_URL = process.env.WEBSITE_URL || "https://www.unison-lang.org"; + +const unisonShareCfg = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + "style-loader", + { + loader: "css-loader", + options: { importLoaders: 1 }, + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [ + postcssPresetEnv({ + features: { + "is-pseudo-class": false, + "custom-media-queries": { + importFrom: `${UI_CORE_SRC}/css/ui/viewport.css`, + }, + }, + }), + ], + }, + }, + }, + ], + }, + { + test: /\.md$/i, + type: "asset/source", + }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: "asset/resource", + }, + { + test: /\.(woff(2)?|ttf|eot)$/i, + type: "asset/resource", + }, + { + test: /\.elm$/, + exclude: [/elm-stuff/, /node_modules/], + use: [ + { + loader: "elm-asset-webpack-loader", + }, + { + loader: "elm-webpack-loader", + options: { + debug: false, + cwd: __dirname, + }, + }, + ], + }, + ], + }, + resolve: { + alias: { + assets: path.resolve(__dirname, "src/assets/"), + "ui-core": path.resolve(__dirname, UI_CORE_SRC + "/"), + }, + }, + + entry: "./src/unisonShare.js", + + plugins: [ + new HtmlWebpackPlugin({ + template: "./src/unisonShare.ejs", + inject: "body", + publicPath: "/static/", + base: "/", + filename: path.resolve(__dirname, "dist/unisonShare/index.html"), + }), + + new FaviconsWebpackPlugin({ + logo: "./src/assets/favicon.svg", + inject: true, + favicons: { + appName: "Unison Share", + appDescription: "Explore, read docs about, and share Unison libraries", + developerName: "Unison", + developerURL: "https://unison-lang.org", + background: "#C6A8EC", + theme_color: "#C6A8EC", + }, + }), + + new CopyPlugin({ + patterns: [ + { + from: "src/assets/unison-share-social.png", + to: "unison-share-social.png", + }, + { + from: "src/assets/unison-logo-circle.png", + to: "unison-logo-circle.png", + }, + { + from: "src/assets/unison-logo-square.png", + to: "unison-logo-square.png", + }, + { + from: "src/assets/unison-cloud-splash.svg", + to: "unison-cloud-splash.svg", + }, + { + from: "src/robots.txt", + to: path.resolve(__dirname, "dist/unisonShare/robots.txt"), + }, + { + from: "src/sitemap.txt", + to: path.resolve(__dirname, "dist/unisonShare/sitemap.txt"), + }, + { + from: "src/404.html", + to: path.resolve(__dirname, "dist/unisonShare/404.html"), + }, + { + from: "src/500.html", + to: path.resolve(__dirname, "dist/unisonShare/500.html"), + }, + { + from: "src/maintenance.html", + to: path.resolve(__dirname, "dist/unisonShare/maintenance.html"), + }, + ], + }), + + new webpack.DefinePlugin({ + API_URL: JSON.stringify(API_URL), + WEBSITE_URL: JSON.stringify(WEBSITE_URL), + APP_ENV: JSON.stringify("production"), + }), + ], + + output: { + filename: "[name].[contenthash].js", + path: path.resolve(__dirname, "dist/unisonShare/static"), + clean: true, + }, +}; + +module.exports = unisonShareCfg;