Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b286ef2a74 | ||
|
|
121937bcbe | ||
|
|
1274bd98df | ||
|
|
1942ffbdcd | ||
|
|
fdff541a2a | ||
|
|
b8a1c5386f | ||
|
|
3c71b8c8f8 | ||
|
|
27ba1bd931 | ||
|
|
2fad1225b0 | ||
|
|
3c968a2bb2 | ||
|
|
e30555c865 | ||
|
|
322df8d47e | ||
|
|
2c56ff2584 | ||
|
|
6d7254f2d2 | ||
|
|
4953fe79fe | ||
|
|
b15925815e | ||
|
|
51d61895a4 | ||
|
|
7f611bd199 | ||
|
|
1b05730c3d | ||
|
|
bf143681ff | ||
|
|
e9979fdad2 | ||
|
|
f1fa827d42 | ||
|
|
b7be3d762b | ||
|
|
7f21132f0c | ||
|
|
d48508b162 | ||
|
|
3ad28eaeb6 | ||
|
|
a87d6260d7 | ||
|
|
6286027559 | ||
|
|
6bf4d487f3 | ||
|
|
a3f9252b90 | ||
|
|
aeee77e9b8 | ||
|
|
8f1980eaa7 | ||
|
|
8cf67b1419 | ||
|
|
15c9640ae1 | ||
|
|
a0a107126b | ||
|
|
4e53b3e6ef | ||
|
|
006d57f7d1 |
@@ -5,7 +5,7 @@
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"baseBranch": "origin/main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@status-im/components": patch
|
||||
---
|
||||
|
||||
fix `<DropdowMenu.SubTrigger />` expanded state
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@status-im/components": patch
|
||||
---
|
||||
|
||||
add `<DropdownMenu.Label />`
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@status-im/components": patch
|
||||
---
|
||||
|
||||
add dark grey dropdown button
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@status-im/components": patch
|
||||
---
|
||||
|
||||
fix `<Button />` text alignment
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@status-im/components": patch
|
||||
---
|
||||
|
||||
Add `<Switch />` and `<DropdownMenu.SwitchItem />`
|
||||
@@ -1,6 +0,0 @@
|
||||
**/dist
|
||||
**/node_modules
|
||||
**/protos
|
||||
**/proto
|
||||
**/coverage
|
||||
**/storybook-static
|
||||
@@ -20,27 +20,37 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
# https://github.com/changesets/changesets/issues/1055
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9.12.3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'yarn'
|
||||
node-version: 20.18.0
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Changeset
|
||||
# https://github.com/changesets/changesets/issues/1048
|
||||
run: pnpm dlx @changesets/cli status --since origin/main
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
run: pnpm build
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint && yarn format --check
|
||||
run: pnpm lint && pnpm format --check
|
||||
|
||||
- name: Test
|
||||
run: yarn test
|
||||
run: pnpm test
|
||||
|
||||
@@ -20,19 +20,22 @@ jobs:
|
||||
needs: ci
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
node-version: 18
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
version: 9.12.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.18.0
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- uses: changesets/action@v1
|
||||
id: changesets
|
||||
with:
|
||||
title: Release
|
||||
commit: Release
|
||||
version: yarn changeset version
|
||||
publish: yarn changeset publish
|
||||
version: pnpm changeset version
|
||||
publish: pnpm changeset publish
|
||||
createGithubReleases: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
pnpm lint-staged
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
20.18.0
|
||||
@@ -0,0 +1,2 @@
|
||||
!.npmrc
|
||||
#!pnpm-lock.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
; https://pnpm.io/cli/run#enable-pre-post-scripts
|
||||
enable-pre-post-scripts=true
|
||||
; https://pnpm.io/npmrc#node-linker
|
||||
; node-linker=hoisted
|
||||
; https://pnpm.io/npmrc#shamefully-hoist
|
||||
;shamefully-hoist=true
|
||||
;auto-install-peers=true
|
||||
; https://pnpm.io/npmrc#node-version
|
||||
node-version=20.18.0
|
||||
; https://pnpm.io/npmrc#engine-strict
|
||||
engine-strict=true
|
||||
@@ -0,0 +1 @@
|
||||
pnpm-lock.yaml
|
||||
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"npm.packageManager": "yarn",
|
||||
"npm.packageManager": "pnpm",
|
||||
"eslint.useESLintClass": true,
|
||||
"eslint.workingDirectories": [
|
||||
"./packages/colors",
|
||||
"./packages/icons",
|
||||
"./packages/components",
|
||||
"./packages/status-js",
|
||||
"./apps/connector",
|
||||
{
|
||||
"mode": "auto",
|
||||
"#comment": "See https://github.com/microsoft/vscode-eslint/issues/1161 for reason (i.e. multiple .eslintrc config files)"
|
||||
|
||||
@@ -6,20 +6,26 @@ This monorepo contains packages for building web applications in the Status ecos
|
||||
|
||||
## Packages
|
||||
|
||||
| Name | `npm` | Description |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`@status-im/components`](./packages/components) | [](https://www.npmjs.com/package/@status-im/components) | Component library built with Radix UI, React Aria, Tailwind CSS. |
|
||||
| [`@status-im/js`](./packages/status-js) | [](https://www.npmjs.com/package/@status-im/js) | Libary for Waku protocol integration and blockchain interactions. |
|
||||
| [`@status-im/icons`](./packages/icons) | [](https://www.npmjs.com/package/@status-im/icons) | Auto-generated icon library based on our [design system](https://www.figma.com/design/qLLuMLfpGxK9OfpIavwsmK/Iconset?node-id=3239-987&node-type=frame&t=0h8iIiZ3Sf0g4MRV-11). |
|
||||
| [`@status-im/colors`](./packages/colors) | [](https://www.npmjs.com/package/@status-im/colors) | Auto-generated color palette based on our [design system](https://www.figma.com/design/v98g9ZiaSHYUdKWrbFg9eM/Foundations?node-id=619-5995&node-type=canvas&m=dev). |
|
||||
| [`@status-im/eslint-config`](./packages/eslint-config) | | Shared ESLint configuration for consistent code style across projects. |
|
||||
| Name | `npm` | Description |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [`@status-im/components`](./packages/components) | [](https://www.npmjs.com/package/@status-im/components) | Component library built with Radix UI, React Aria, Tailwind CSS. |
|
||||
| [`@status-im/js`](./packages/status-js) | [](https://www.npmjs.com/package/@status-im/js) | Libary for Waku protocol integration and blockchain interactions. |
|
||||
| [`@status-im/icons`](./packages/icons) | [](https://www.npmjs.com/package/@status-im/icons) | Auto-generated icon library based on our [design system](https://www.figma.com/design/qLLuMLfpGxK9OfpIavwsmK/Iconset?node-id=3239-987&node-type=frame&t=0h8iIiZ3Sf0g4MRV-11). |
|
||||
| [`@status-im/colors`](./packages/colors) | [](https://www.npmjs.com/package/@status-im/colors) | Auto-generated color palette based on our [design system](https://www.figma.com/design/v98g9ZiaSHYUdKWrbFg9eM/Foundations?node-id=619-5995&node-type=canvas&m=dev). |
|
||||
| [`@status-im/eslint-config`](./packages/eslint-config) | [](https://www.npmjs.com/package/@status-im/eslint-config) | Shared ESLint configuration for consistent code style across projects. |
|
||||
|
||||
## Apps
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| [`./apps/connector`](./apps/connector) | Status Desktop Wallet extended to decentralised applications in your browser. |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Required:
|
||||
|
||||
- **[Node.js](https://nodejs.org/)** v18.x
|
||||
- **[Yarn](https://yarnpkg.com/)** v1.22.x
|
||||
- **[Node.js](https://nodejs.org/)** v20.x
|
||||
- **[pnpm](https://pnpm.io)** v9.12.x
|
||||
|
||||
Recommended:
|
||||
|
||||
@@ -51,24 +57,24 @@ Recommended:
|
||||
2. Install dependencies:
|
||||
|
||||
```
|
||||
yarn install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Build all packages:
|
||||
|
||||
```
|
||||
yarn build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
4. Run tests:
|
||||
|
||||
```
|
||||
yarn test
|
||||
pnpm test
|
||||
```
|
||||
|
||||
5. Start development mode:
|
||||
```
|
||||
yarn dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Storybook
|
||||
@@ -76,7 +82,7 @@ Recommended:
|
||||
To view and interact with the components, you can run Storybook:
|
||||
|
||||
```
|
||||
yarn storybook
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
This will start the Storybook server, allowing you to browse and test components in isolation.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
#cache
|
||||
.turbo
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
!.env.*
|
||||
.env*.local
|
||||
|
||||
out/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# plasmo - https://www.plasmo.com
|
||||
.plasmo
|
||||
|
||||
# bpp - http://bpp.browser.market/
|
||||
keys.json
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
apps/
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"tailwindConfig": "./tailwind.config.ts",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @type {import('prettier').Options}
|
||||
*/
|
||||
export default {
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: false,
|
||||
singleQuote: false,
|
||||
trailingComma: 'none',
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: true,
|
||||
plugins: ['@ianvs/prettier-plugin-sort-imports'],
|
||||
importOrder: [
|
||||
'<BUILTIN_MODULES>', // Node.js built-in modules
|
||||
'<THIRD_PARTY_MODULES>', // Imports not matched by other special words or groups.
|
||||
'', // Empty line
|
||||
'^@plasmo/(.*)$',
|
||||
'',
|
||||
'^@plasmohq/(.*)$',
|
||||
'',
|
||||
'^~(.*)$',
|
||||
'',
|
||||
'^[./]',
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# connector
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3c71b8c: update ESLint
|
||||
- Updated dependencies [3c71b8c]
|
||||
- @status-im/colors@1.0.2
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env groovy
|
||||
library 'status-jenkins-lib@v1.9.11'
|
||||
|
||||
def changesDetected = false
|
||||
|
||||
pipeline {
|
||||
agent { label 'linux' }
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
/* Prevent Jenkins jobs from running forever */
|
||||
timeout(time: 10, unit: 'MINUTES')
|
||||
/* manage how many builds we keep */
|
||||
buildDiscarder(logRotator(
|
||||
numToKeepStr: '20',
|
||||
daysToKeepStr: '30',
|
||||
))
|
||||
disableConcurrentBuilds()
|
||||
}
|
||||
|
||||
environment {
|
||||
PLATFORM = 'chrome'
|
||||
ZIP_NAME = utils.pkgFilename(
|
||||
type: 'Extension',
|
||||
version: 'none',
|
||||
arch: 'chrome',
|
||||
ext: 'zip',
|
||||
)
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Check Changed Files') {
|
||||
when {
|
||||
changeset pattern: "apps/connector/**", comparator: "GLOB"
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
changesDetected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install') {
|
||||
when {
|
||||
expression { changesDetected }
|
||||
}
|
||||
steps {
|
||||
dir("${env.WORKSPACE}/apps/connector") {
|
||||
script {
|
||||
nix.shell(
|
||||
'pnpm install --frozen-lockfile',
|
||||
pure: false,
|
||||
entryPoint: "${env.WORKSPACE}/apps/connector/shell.nix"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build') {
|
||||
when {
|
||||
expression { changesDetected }
|
||||
}
|
||||
steps {
|
||||
dir("${env.WORKSPACE}") {
|
||||
script {
|
||||
nix.shell(
|
||||
'pnpm turbo run build --filter=connector',
|
||||
pure: false,
|
||||
entryPoint: "${env.WORKSPACE}/apps/connector/shell.nix"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Zip') {
|
||||
when {
|
||||
expression { changesDetected }
|
||||
}
|
||||
steps {
|
||||
dir("${env.WORKSPACE}/apps/connector") {
|
||||
zip(
|
||||
zipFile: env.ZIP_NAME,
|
||||
dir: 'build/chrome-mv3-prod',
|
||||
archive: false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
when {
|
||||
expression { changesDetected }
|
||||
}
|
||||
steps {
|
||||
dir("${env.WORKSPACE}/apps/connector") {
|
||||
archiveArtifacts(
|
||||
artifacts: env.ZIP_NAME,
|
||||
fingerprint: true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Upload') {
|
||||
when {
|
||||
expression { changesDetected }
|
||||
}
|
||||
steps {
|
||||
dir("${env.WORKSPACE}/apps/connector") {
|
||||
script {
|
||||
env.PKG_URL = s5cmd.upload(env.ZIP_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
script {
|
||||
if(changesDetected) {
|
||||
github.notifyPR(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
failure {
|
||||
script {
|
||||
if(changesDetected) {
|
||||
github.notifyPR(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup { cleanWs() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
### 1. Definitions
|
||||
|
||||
**1.1. “Contributor”**
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
**1.2. “Contributor Version”**
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
**1.3. “Contribution”**
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
**1.4. “Covered Software”**
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
**1.5. “Incompatible With Secondary Licenses”**
|
||||
means
|
||||
|
||||
* **(a)** that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
* **(b)** that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
**1.6. “Executable Form”**
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
**1.7. “Larger Work”**
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
**1.8. “License”**
|
||||
means this document.
|
||||
|
||||
**1.9. “Licensable”**
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
**1.10. “Modifications”**
|
||||
means any of the following:
|
||||
|
||||
* **(a)** any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
* **(b)** any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
**1.11. “Patent Claims” of a Contributor**
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
**1.12. “Secondary License”**
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
**1.13. “Source Code Form”**
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
**1.14. “You” (or “Your”)**
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, “You” includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, “control” means **(a)** the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or **(b)** ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
|
||||
### 2. License Grants and Conditions
|
||||
|
||||
#### 2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
* **(a)** under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
* **(b)** under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
#### 2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
#### 2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
* **(a)** for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
* **(b)** for infringements caused by: **(i)** Your and any other third party's
|
||||
modifications of Covered Software, or **(ii)** the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
* **(c)** under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
#### 2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
#### 2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
#### 2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
#### 2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
|
||||
### 3. Responsibilities
|
||||
|
||||
#### 3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
#### 3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
* **(a)** such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
* **(b)** You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
#### 3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
#### 3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
#### 3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
|
||||
### 4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: **(a)** comply with
|
||||
the terms of this License to the maximum extent possible; and **(b)**
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
|
||||
### 5. Termination
|
||||
|
||||
**5.1.** The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated **(a)** provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and **(b)** on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
**5.2.** If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
|
||||
### 6. Disclaimer of Warranty
|
||||
|
||||
> Covered Software is provided under this License on an “as is”
|
||||
> basis, without warranty of any kind, either expressed, implied, or
|
||||
> statutory, including, without limitation, warranties that the
|
||||
> Covered Software is free of defects, merchantable, fit for a
|
||||
> particular purpose or non-infringing. The entire risk as to the
|
||||
> quality and performance of the Covered Software is with You.
|
||||
> Should any Covered Software prove defective in any respect, You
|
||||
> (not any Contributor) assume the cost of any necessary servicing,
|
||||
> repair, or correction. This disclaimer of warranty constitutes an
|
||||
> essential part of this License. No use of any Covered Software is
|
||||
> authorized under this License except under this disclaimer.
|
||||
|
||||
### 7. Limitation of Liability
|
||||
|
||||
> Under no circumstances and under no legal theory, whether tort
|
||||
> (including negligence), contract, or otherwise, shall any
|
||||
> Contributor, or anyone who distributes Covered Software as
|
||||
> permitted above, be liable to You for any direct, indirect,
|
||||
> special, incidental, or consequential damages of any character
|
||||
> including, without limitation, damages for lost profits, loss of
|
||||
> goodwill, work stoppage, computer failure or malfunction, or any
|
||||
> and all other commercial damages or losses, even if such party
|
||||
> shall have been informed of the possibility of such damages. This
|
||||
> limitation of liability shall not apply to liability for death or
|
||||
> personal injury resulting from such party's negligence to the
|
||||
> extent applicable law prohibits such limitation. Some
|
||||
> jurisdictions do not allow the exclusion or limitation of
|
||||
> incidental or consequential damages, so this exclusion and
|
||||
> limitation may not apply to You.
|
||||
|
||||
|
||||
### 8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
|
||||
### 9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
### 10. Versions of the License
|
||||
|
||||
#### 10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
#### 10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
#### 10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
## Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
## Exhibit B - “Incompatible With Secondary Licenses” Notice
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,104 @@
|
||||
# A wallet connector by Status
|
||||
|
||||
Status Desktop Wallet extended to decentralized applications in your browser.
|
||||
|
||||
## Compatibility
|
||||
|
||||
Depends on:
|
||||
|
||||
- **[Status Desktop](https://github.com/status-im/status-desktop/releases)** >=2.32.x
|
||||
|
||||
Tested with these browsers:
|
||||
|
||||
- Google Chrome
|
||||
- Arc
|
||||
|
||||
## Development
|
||||
|
||||
### Google Chrome
|
||||
|
||||
#### Develop
|
||||
|
||||
```bash
|
||||
pnpm dev:chrome
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
pnpm build:chrome
|
||||
```
|
||||
|
||||
#### Load
|
||||
|
||||
Google Chrome > Window > Extensions > enable Developer mode
|
||||
|
||||
Google Chrome > Window > Extensions > Load unpacked > select build (build/chrome-mv3-dev)
|
||||
|
||||
> Note: Reloads automatically in development.
|
||||
|
||||
### Safari
|
||||
|
||||
#### Develop
|
||||
|
||||
```bash
|
||||
pnpm dev:safari
|
||||
```
|
||||
|
||||
#### Convert
|
||||
|
||||
```bash
|
||||
xcrun safari-web-extension-converter --no-open --macos-only --swift --project-location ./apps --app-name Status --bundle-identifier im.Status.Status ./build/safari-mv3-dev/
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
xcodebuild -project apps/Status/Status.xcodeproj -scheme Status build
|
||||
```
|
||||
|
||||
#### Load
|
||||
|
||||
Safari > Settings... > Developer > Allow unsigned extensions
|
||||
|
||||
Safari > Settings... > Extensions > check Status
|
||||
|
||||
> Note: Does not reload automatically, requires build on change and converting as well if adding new files.
|
||||
|
||||
### Firefox Developer Edition
|
||||
|
||||
#### Develop
|
||||
|
||||
```bash
|
||||
pnpm dev:firefox
|
||||
```
|
||||
|
||||
#### Build
|
||||
|
||||
```bash
|
||||
pnpm build:firefox
|
||||
```
|
||||
|
||||
#### Load
|
||||
|
||||
https://support.mozilla.org/en-US/kb/add-on-signing-in-firefox#w_what-are-my-options-if-i-want-to-use-an-unsigned-add-on-advanced-users
|
||||
|
||||
Firefox Developer Edition > Tools > Add-ons and Themes > click on gear icon (Tools for all add-ons) > Install Add-on From File... > select build (build/firefox-mv3-prod.zip)
|
||||
|
||||
> Note: Does not reload automatically, requires build and load on change.
|
||||
|
||||
### Status Desktop
|
||||
|
||||
#### Run
|
||||
|
||||
##### macOS
|
||||
|
||||
```bash
|
||||
FLAG_CONNECTOR_ENABLED=1 '/Volumes/Status Desktop/Status.app/Contents/MacOS/nim_status_client'
|
||||
```
|
||||
|
||||
> Tip: Point to the installer package location without dropping and replacing current app.
|
||||
|
||||
## Testing
|
||||
|
||||
Download latest build from last merged PR or build from source. To use the extension see the load steps from [Development](#development) section.
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,41 @@
|
||||
import configs, { tailwindcssConfigs } from '@status-im/eslint-config'
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
...configs,
|
||||
...tailwindcssConfigs,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'no-constant-binary-expression': 'error',
|
||||
'no-restricted-globals': ['error', 'process'],
|
||||
'jsx-a11y/alt-text': [
|
||||
1,
|
||||
{
|
||||
img: [],
|
||||
},
|
||||
],
|
||||
'no-empty': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.mjs'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['.plasmo'],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { Provider } from './src/contents/provider'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ethereum: Provider
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "connector",
|
||||
"version": "0.0.2",
|
||||
"license": "MPL-2.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/status-im/status-web.git",
|
||||
"directory": "apps/connector"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev:chrome": "plasmo dev --target=chrome-mv3",
|
||||
"dev:safari": "plasmo dev --target=safari-mv3",
|
||||
"dev:firefox": "plasmo dev --target=firefox-mv3",
|
||||
"build": "pnpm build:chrome",
|
||||
"build:chrome": "plasmo build --target=chrome-mv3",
|
||||
"build:firefox": "plasmo build --target=firefox-mv3 --zip",
|
||||
"lint": "eslint src",
|
||||
"format": "prettier --write .",
|
||||
"package": "plasmo package",
|
||||
"clean": "rimraf apps build .plasmo node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@plasmohq/messaging": "^0.6.2",
|
||||
"@plasmohq/storage": "^1.11.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@status-im/colors": "workspace:*",
|
||||
"cva": "^1.0.0-beta.1",
|
||||
"ethers": "^6.13.0",
|
||||
"plasmo": "0.88.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"ts-pattern": "^5.2.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.1.1",
|
||||
"@parcel/bundler-experimental": "^2.7.0",
|
||||
"@status-im/eslint-config": "workspace:*",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/chrome": "0.0.258",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"husky": "^8.0.3",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "3.2.4",
|
||||
"prettier-plugin-tailwindcss": "^0.6.1",
|
||||
"rimraf": "^4.4.1",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwind-scrollbar-utilities": "^0.2.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"manifest": {
|
||||
"manifest_version": 3,
|
||||
"name": "A wallet connector by Status",
|
||||
"description": "Status Desktop Wallet extended to decentralised applications in your browser.",
|
||||
"externally_connectable": {
|
||||
"ids": [],
|
||||
"matches": []
|
||||
},
|
||||
"host_permissions": [
|
||||
"https://*/*"
|
||||
],
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "no-reply@status.im"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx,mjs}": [
|
||||
"eslint",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{yml,yaml,json}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
source ? builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/df27247e6f3e636c119e2610bf12d38b5e98cc79.tar.gz";
|
||||
sha256 = "sha256:0bbvimk7xb7akrx106mmsiwf9nzxnssisqmqffla03zz51d0kz2n";
|
||||
},
|
||||
pkgs ? import (source) {}
|
||||
}:
|
||||
|
||||
pkgs.mkShell {
|
||||
name = "browser-extension-shell";
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
nodejs_20
|
||||
pnpm
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Storage } from '@plasmohq/storage'
|
||||
import iconConnected from 'url:../assets/icon-connected.png' // eslint-disable-line import/no-unresolved
|
||||
import iconDisconnected from 'url:../assets/icon-disconnected.png' // eslint-disable-line import/no-unresolved
|
||||
|
||||
import { config } from '~config'
|
||||
|
||||
import type { ServiceWorkerMessage } from '~messages/service-worker-message'
|
||||
|
||||
/**
|
||||
* Check if the WebSocket server is reachable
|
||||
*/
|
||||
const storage = new Storage()
|
||||
|
||||
const DESKTOP_ENDPOINT_URL = config.desktop.rpc.url.replace('ws:', 'http:')
|
||||
const CHECK_INTERVAL_MS = 5000
|
||||
|
||||
const isWebSocketServerReachable = async (): Promise<boolean> => {
|
||||
try {
|
||||
await fetch(DESKTOP_ENDPOINT_URL, { method: 'HEAD' })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDesktopStatus() {
|
||||
const isServerReachable = await isWebSocketServerReachable()
|
||||
storage.set('status:desktop:running', isServerReachable)
|
||||
chrome.action.setIcon({
|
||||
path: isServerReachable ? iconConnected : iconDisconnected,
|
||||
})
|
||||
}
|
||||
|
||||
checkDesktopStatus()
|
||||
setInterval(checkDesktopStatus, CHECK_INTERVAL_MS)
|
||||
|
||||
/**
|
||||
* Handler for opening popup
|
||||
*/
|
||||
chrome.action.onClicked.addListener(async tab => {
|
||||
if (!tab.id || !tab.url) {
|
||||
return
|
||||
}
|
||||
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'status:icon:clicked',
|
||||
} satisfies ServiceWorkerMessage)
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useLocalStorage } from '~hooks/use-local-storage'
|
||||
import { getFaviconUrl } from '~lib/get-favicon-url'
|
||||
|
||||
import { Network } from './network'
|
||||
import { Switch } from './switch'
|
||||
|
||||
const Connected = () => {
|
||||
const [defaultWallet, setDefaultWallet] = useLocalStorage(
|
||||
'status:default-wallet',
|
||||
true,
|
||||
)
|
||||
const [, setReopen] = useLocalStorage('status:popup:reopen', false)
|
||||
|
||||
const dappUrl = window.location.hostname
|
||||
const dappIcon = getFaviconUrl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* fixme: set default favicon `onerror` */}
|
||||
{dappIcon ? (
|
||||
<img src={dappIcon} alt="icon" className="size-8 rounded-full" />
|
||||
) : (
|
||||
<div className="size-8 rounded-full bg-neutral-5" />
|
||||
)}
|
||||
<div className="text-27 font-semibold">{dappUrl}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<Network />
|
||||
|
||||
<div className="flex items-center gap-2 rounded-2xl border border-neutral-20 bg-neutral-2.5 px-4 py-3">
|
||||
<div className="flex-1">
|
||||
<div className="text-15 font-medium text-neutral-100">
|
||||
Set as default wallet
|
||||
</div>
|
||||
<div className="text-13 font-regular text-neutral-50">
|
||||
Launch Status (Desktop) when connecting to dApps with MetaMask or
|
||||
other wallets
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={defaultWallet}
|
||||
onCheckedChange={checked => {
|
||||
setDefaultWallet(checked)
|
||||
setReopen(true)
|
||||
window.location.reload()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { Connected }
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
Root,
|
||||
Trigger,
|
||||
} from '@radix-ui/react-dropdown-menu'
|
||||
import { cx } from 'cva'
|
||||
|
||||
type Props = {
|
||||
children: [React.ReactElement, React.ReactElement]
|
||||
modal?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
const DropdownMenu = (props: Props) => {
|
||||
const { children, onOpenChange, modal } = props
|
||||
|
||||
const [trigger, content] = children
|
||||
|
||||
return (
|
||||
<Root onOpenChange={onOpenChange} modal={modal}>
|
||||
<Trigger asChild>{trigger}</Trigger>
|
||||
{content}
|
||||
</Root>
|
||||
)
|
||||
}
|
||||
|
||||
type DropdownMenuItemProps = {
|
||||
label: string
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
const MenuItem = (props: DropdownMenuItemProps) => {
|
||||
const { label, onSelect } = props
|
||||
|
||||
return (
|
||||
<ItemBase onSelect={onSelect}>
|
||||
<div className="flex items-center gap-8">
|
||||
<p className="text-15 font-medium text-neutral-100">{label}</p>
|
||||
</div>
|
||||
</ItemBase>
|
||||
)
|
||||
}
|
||||
|
||||
const Content = (props: React.ComponentProps<typeof DropdownMenuContent>) => {
|
||||
return (
|
||||
<DropdownMenuContent
|
||||
{...props}
|
||||
className={cx('w-64 p-1 rounded-xl bg-white-100', 'shadow-3 ')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemBase = (props: React.ComponentProps<typeof DropdownMenuItem>) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
{...props}
|
||||
className="flex h-8 cursor-pointer select-none items-center justify-between gap-2 rounded-[10px] px-2 transition-colors hover:bg-neutral-5"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Separator = (
|
||||
props: React.ComponentProps<typeof DropdownMenuSeparator>,
|
||||
) => {
|
||||
return (
|
||||
<DropdownMenuSeparator
|
||||
{...props}
|
||||
className="mx-[-4px] my-1 h-px bg-neutral-10"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu.Content = Content
|
||||
DropdownMenu.Item = MenuItem
|
||||
DropdownMenu.Separator = Separator
|
||||
|
||||
export { DropdownMenu }
|
||||
export type DropdownMenuProps = Omit<Props, 'children'>
|
||||
@@ -0,0 +1,93 @@
|
||||
import { cx } from 'cva'
|
||||
|
||||
import { config } from '~config'
|
||||
|
||||
import { DropdownMenu } from './dropdown-menu'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
function downloadUrl(dataurl: string) {
|
||||
const link = document.createElement('a')
|
||||
link.href = dataurl
|
||||
link.target = '_blank'
|
||||
link.click()
|
||||
}
|
||||
|
||||
const MacOsPicker = (props: Props) => {
|
||||
const { children, className } = props
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<button
|
||||
className={cx(
|
||||
'group flex items-center justify-center gap-1 whitespace-nowrap rounded-xl border text-center text-15 font-medium leading-normal outline-none transition-all',
|
||||
'h-10 px-3',
|
||||
'border-neutral-30 bg-white-100 text-neutral-100 hover:border-neutral-40 active:border-neutral-50',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="-mt-px">
|
||||
<svg
|
||||
color="rgba(9 16 28 / 100%)"
|
||||
width="20"
|
||||
height="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="rgba(9 16 28 / 100%)"
|
||||
d="M14.357 10c.023 2.422 2.12 3.227 2.143 3.238-.018.056-.335 1.148-1.105 2.275-.665.975-1.356 1.946-2.444 1.966-1.069.02-1.413-.635-2.635-.635-1.222 0-1.604.615-2.616.655-1.05.04-1.85-1.054-2.52-2.025-1.371-1.987-2.42-5.613-1.012-8.062.699-1.215 1.948-1.985 3.303-2.005 1.031-.02 2.005.695 2.635.695.63 0 1.813-.86 3.056-.733.521.022 1.982.21 2.92 1.587-.075.047-1.743 1.02-1.725 3.044Zm-2.009-5.945c.558-.677.933-1.618.83-2.555-.803.032-1.775.537-2.351 1.213-.517.598-.97 1.556-.847 2.475.895.069 1.81-.457 2.368-1.133Z"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
{children}
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
className="-ml-1 transition-transform duration-200 group-aria-expanded:rotate-180"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10Z"
|
||||
fill="#E7EAEE"
|
||||
fillOpacity={1}
|
||||
/>
|
||||
<path
|
||||
d="M7 8.5L10 11.5L13 8.5"
|
||||
stroke={'#09101C'}
|
||||
strokeWidth="1.2"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<DropdownMenu.Content
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="[&>div]:outline-none"
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
label="Apple Silicon"
|
||||
onSelect={() =>
|
||||
downloadUrl(config.desktop.downloadUrls.macos.silicon)
|
||||
}
|
||||
/>
|
||||
<DropdownMenu.Item
|
||||
label="Intel"
|
||||
onSelect={() => downloadUrl(config.desktop.downloadUrls.macos.intel)}
|
||||
/>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export { MacOsPicker }
|
||||
@@ -0,0 +1,39 @@
|
||||
import arbitrumImage from 'url:../../assets/network/arbitrum.webp'
|
||||
import mainnetImage from 'url:../../assets/network/mainnet.webp'
|
||||
import optimismImage from 'url:../../assets/network/optimism.webp'
|
||||
|
||||
const Network = () => {
|
||||
return (
|
||||
<div className="flex h-[64px] items-center rounded-2xl border border-neutral-10 bg-white-100 px-4">
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="text-13 font-regular text-neutral-50">
|
||||
Supported networks
|
||||
</div>
|
||||
<div className="text-15 font-medium text-neutral-100">
|
||||
Mainnet, Optimism, Arbitrum
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<img
|
||||
className="box-content size-6 rounded-full border border-white-100"
|
||||
src={mainnetImage}
|
||||
alt="Mainnet"
|
||||
/>
|
||||
<img
|
||||
className="ml-[-5px] box-content size-6 rounded-full border border-white-100"
|
||||
src={optimismImage}
|
||||
alt="Optimism"
|
||||
/>
|
||||
<img
|
||||
className="ml-[-5px] box-content size-6 rounded-full border border-white-100"
|
||||
src={arbitrumImage}
|
||||
alt="Arbitrum"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { Network }
|
||||
@@ -0,0 +1,21 @@
|
||||
import unableToConnectImage from 'url:../../assets/unable-to-connect.png'
|
||||
|
||||
import { DownloadButton } from './download-button'
|
||||
|
||||
const NotConnected = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<img src={unableToConnectImage} alt="Not connected" className="w-full" />
|
||||
<div className="max-w-[304px] pb-5 pt-4 text-center">
|
||||
<p className="text-19 font-semibold">Unable to connect to Status</p>
|
||||
<p className="text-15">
|
||||
Make sure Status desktop app is running on your machine or download it
|
||||
below.
|
||||
</p>
|
||||
</div>
|
||||
<DownloadButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { NotConnected }
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useStorage } from '@plasmohq/storage/hook'
|
||||
|
||||
const PinInstructions = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_showPinInstructions, setShowPinInstructions] = useStorage(
|
||||
'show-pin-instructions',
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="-mx-5 mt-8 flex items-center gap-2 border-t border-neutral-80/5 px-4 pt-3">
|
||||
<div className="flex-1">
|
||||
<div className="text-15 font-semibold text-neutral-100">
|
||||
Pin the extension
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-13 font-regular">
|
||||
Click <PuzzleIcon /> and then <PinIcon /> and voilà!
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="group inline-flex h-8 items-center justify-center gap-1 whitespace-nowrap rounded-xl border border-neutral-30 bg-white-100 px-3 text-center text-15 font-medium leading-normal text-neutral-100 outline-none transition-all hover:border-neutral-40 active:border-neutral-50"
|
||||
onClick={() => setShowPinInstructions(false)}
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { PinInstructions }
|
||||
|
||||
const PuzzleIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none">
|
||||
<path
|
||||
fill="#09101C"
|
||||
fillRule="evenodd"
|
||||
d="M7.5 2.6a.9.9 0 0 0-.9.9v.6H5V2.9h.487a2.1 2.1 0 0 1 4.026 0h.611c.4 0 .735 0 1.01.023.287.023.56.074.82.206a2.1 2.1 0 0 1 .917.918c.132.26.183.532.207.82.022.274.022.61.022 1.01v.61a2.1 2.1 0 0 1 0 4.026v.612c0 .4 0 .735-.022 1.01-.024.287-.075.56-.207.819a2.1 2.1 0 0 1-.918.918c-.259.132-.532.183-.819.206-.275.023-.61.023-1.01.023H4.876c-.4 0-.735 0-1.01-.023-.287-.023-.56-.074-.82-.206a2.1 2.1 0 0 1-.917-.918c-.132-.26-.183-.532-.207-.82-.022-.274-.022-.61-.022-1.01V9.4h.6a.9.9 0 1 0 0-1.8h-.6V5.876c0-.4 0-.735.022-1.01.024-.287.075-.56.207-.819a2.1 2.1 0 0 1 .918-.918c.259-.132.532-.183.819-.206.275-.022.61-.022 1.01-.022H5v1.2h-.1c-.43 0-.716 0-.936.018-.213.017-.31.048-.373.08a.9.9 0 0 0-.393.393c-.031.062-.062.16-.08.372-.018.22-.018.507-.018.937v.586a2.1 2.1 0 0 1 0 4.026v.588c0 .43 0 .716.018.936.018.213.049.31.08.372a.9.9 0 0 0 .393.393c.062.032.16.063.373.08.22.018.506.018.936.018h5.2c.43 0 .716 0 .936-.018.213-.017.31-.048.373-.08a.9.9 0 0 0 .393-.393c.031-.062.062-.16.08-.372.017-.22.018-.507.018-.937V9.4h.6a.9.9 0 1 0 0-1.8h-.6V5.9c0-.43 0-.716-.018-.936-.018-.213-.049-.31-.08-.372a.9.9 0 0 0-.393-.393c-.062-.032-.16-.063-.373-.08-.22-.018-.506-.018-.936-.018H8.4v-.6a.9.9 0 0 0-.9-.9Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const PinIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none">
|
||||
<g clipPath="url(#a)">
|
||||
<path
|
||||
fill="#09101C"
|
||||
fillRule="evenodd"
|
||||
d="M6.657 5.955c.497-.86.897-1.491 1.25-1.942.355-.454.625-.677.846-.78.377-.176.821-.097 1.932.545 1.111.641 1.402.986 1.439 1.4.02.243-.037.59-.253 1.124-.214.53-.56 1.193-1.057 2.053l-.073.126-.007.145-.173 3.53L3.58 8.123l2.896-1.973.113-.077.069-.119Zm4.628-3.217c-1.054-.608-2.033-1.062-3.04-.593-.47.22-.88.613-1.284 1.129-.387.495-.8 1.147-1.274 1.962L2.114 7.67l-.789.538.827.478 3.744 2.161-1.886 3.114h1.5l1.438-2.506 3.864 2.23.852.492.048-.981.214-4.367c.467-.815.823-1.497 1.058-2.078.245-.607.38-1.16.335-1.677-.097-1.105-.98-1.727-2.034-2.336Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h16v16H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
import { cva } from 'cva'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
type Props = {
|
||||
status: 'on' | 'off'
|
||||
}
|
||||
|
||||
const styles = cva({
|
||||
base: 'flex h-6 items-center gap-1 rounded-[20px] border px-2 text-13',
|
||||
variants: {
|
||||
status: {
|
||||
on: 'border-success-50/20 bg-success-50/10 text-success-60',
|
||||
off: 'border-danger-50/20 bg-danger-50/10 text-danger-60',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StatusTag = (props: Props) => {
|
||||
const { status } = props
|
||||
|
||||
return (
|
||||
<div className={styles({ status })}>
|
||||
{match(status)
|
||||
.with('on', () => (
|
||||
<>
|
||||
<svg
|
||||
width={12}
|
||||
height={12}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 1.5l-1.5 3H10L4.5 10 5 7H2l2.5-5.5H8z"
|
||||
stroke="#1C8A80"
|
||||
strokeWidth={1.1}
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-success-60">Status desktop running</span>
|
||||
</>
|
||||
))
|
||||
.with('off', () => (
|
||||
<>
|
||||
<svg
|
||||
width={12}
|
||||
height={12}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.45 1v4.5h1.1V1h-1.1zm2.525 1.58a3.95 3.95 0 11-3.95 0l-.55-.954a5.05 5.05 0 105.05 0l-.55.953z"
|
||||
fill="#BA434D"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-danger-60">Status desktop not running</span>
|
||||
</>
|
||||
))
|
||||
.exhaustive()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { StatusTag }
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as BaseSwitch from '@radix-ui/react-switch'
|
||||
|
||||
type Props = {
|
||||
checked: boolean
|
||||
onCheckedChange: (checked: boolean) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const Switch = (props: Props) => {
|
||||
return (
|
||||
<BaseSwitch.Root
|
||||
className="relative h-[20px] w-[30px] cursor-default rounded-full bg-neutral-30 outline-none data-[disabled]:cursor-not-allowed data-[state=checked]:bg-customisation-blue-50 data-[disabled]:opacity-30"
|
||||
{...props}
|
||||
>
|
||||
<BaseSwitch.Thumb className="block size-[16px] translate-x-0.5 rounded-full bg-white-100 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[12px]" />
|
||||
</BaseSwitch.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,17 @@
|
||||
export const config = {
|
||||
desktop: {
|
||||
rpc: {
|
||||
url: 'ws://localhost:8586',
|
||||
method: 'connector_callRPC',
|
||||
},
|
||||
downloadUrls: {
|
||||
macos: {
|
||||
silicon:
|
||||
'https://status.app/api/download/macos-silicon?source=connector',
|
||||
intel: 'https://status.app/api/download/macos-intel?source=connector',
|
||||
},
|
||||
windows: 'https://status.app/api/download/windows?source=connector',
|
||||
linux: 'https://status.app/api/download/linux?source=connector',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url(data-base64:../../assets/fonts/Inter.woff2) format('woff2');
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useStorage } from '@plasmohq/storage/hook'
|
||||
import styleText from 'data-text:../style.css' // eslint-disable-line import/no-unresolved
|
||||
import logoSrc from 'url:../../assets/logo.png'
|
||||
|
||||
import { Connected } from '~components/connected'
|
||||
import { NotConnected } from '~components/not-connected'
|
||||
import { PinInstructions } from '~components/pin-instructions'
|
||||
import { StatusTag } from '~components/status-tag'
|
||||
import { useLocalStorage } from '~hooks/use-local-storage'
|
||||
import { useOutsideClick } from '~hooks/use-outside-click'
|
||||
import { ServiceWorkerMessage } from '~messages/service-worker-message'
|
||||
|
||||
import type { PlasmoGetStyle } from 'plasmo'
|
||||
|
||||
export const getStyle: PlasmoGetStyle = () => {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = styleText
|
||||
return style
|
||||
}
|
||||
|
||||
type MessageHandler = Parameters<typeof chrome.runtime.onMessage.addListener>[0]
|
||||
|
||||
export default function Popup() {
|
||||
const [connected] = useStorage('status:desktop:running')
|
||||
const [showPinInstructions] = useStorage('show-pin-instructions', true)
|
||||
|
||||
const [reopen, setReopen] = useLocalStorage('status:popup:reopen', false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const handleOutsideClick = useCallback(() => setOpen(false), [])
|
||||
|
||||
useOutsideClick(containerRef, handleOutsideClick)
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage: MessageHandler = (message: ServiceWorkerMessage) => {
|
||||
try {
|
||||
message = ServiceWorkerMessage.parse(message)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type === 'status:icon:clicked') {
|
||||
setOpen(open => !open)
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(handleMessage)
|
||||
|
||||
return () => {
|
||||
chrome.runtime.onMessage.removeListener(handleMessage)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (reopen) {
|
||||
setOpen(true)
|
||||
setReopen(false)
|
||||
}
|
||||
}, [reopen, setReopen])
|
||||
|
||||
if (!open) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="fixed right-2 top-2 max-w-[390px] overflow-hidden rounded-3xl"
|
||||
>
|
||||
<div className="grid w-full grid-rows-[auto,1fr] rounded-3xl bg-neutral-100">
|
||||
{/* Bar */}
|
||||
<div className="flex items-center justify-between px-5 py-3">
|
||||
<img src={logoSrc} alt="Status" className="size-8" />
|
||||
<StatusTag status={connected ? 'on' : 'off'} />
|
||||
</div>
|
||||
|
||||
<div className="mx-1 mb-1 overflow-hidden rounded-[20px] bg-white-100 p-5">
|
||||
{connected ? <Connected /> : <NotConnected />}
|
||||
{showPinInstructions && <PinInstructions />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import { ProviderRpcError } from '~lib/provider-rpc-error'
|
||||
import { RequestArguments } from '~lib/request-arguments'
|
||||
import { ProxyMessage } from '~messages/proxy-message'
|
||||
|
||||
import type { ProviderMessage } from '~messages/provider-message'
|
||||
import type { PlasmoCSConfig } from 'plasmo'
|
||||
|
||||
export const config: PlasmoCSConfig = {
|
||||
matches: ['https://*/*'],
|
||||
world: 'MAIN',
|
||||
run_at: 'document_start',
|
||||
all_frames: false,
|
||||
}
|
||||
|
||||
type Event =
|
||||
| 'connect'
|
||||
| 'connected'
|
||||
| 'disconnect'
|
||||
| 'close'
|
||||
| 'error'
|
||||
| 'chainChanged'
|
||||
| 'accountsChanged'
|
||||
| 'networkChanged'
|
||||
|
||||
/**
|
||||
* @see https://eips.ethereum.org/EIPS/eip-1193 for spec
|
||||
*/
|
||||
export class Provider {
|
||||
#listeners: Map<Event, (...args: unknown[]) => void>
|
||||
|
||||
/**
|
||||
* @see https://github.com/snapshot-labs/lock/blob/503f4b07f1b631b1eed0dca993110dc561189261/src/utils.ts for other examples
|
||||
*/
|
||||
public isStatus: boolean
|
||||
|
||||
public isMetaMask: boolean
|
||||
public _metamask: {
|
||||
isUnlocked: () => Promise<true>
|
||||
} | null
|
||||
|
||||
// public isCoinbaseWallet: boolean
|
||||
public qrUrl: null
|
||||
|
||||
public autoRefreshOnNetworkChange: boolean
|
||||
|
||||
public provider: Record<string, unknown> | null
|
||||
|
||||
public __isProvider: boolean
|
||||
|
||||
public connected: boolean
|
||||
|
||||
constructor() {
|
||||
this.isStatus = true
|
||||
this.isMetaMask = false
|
||||
this._metamask = null
|
||||
// this.isCoinbaseWallet = false
|
||||
this.qrUrl = null
|
||||
this.autoRefreshOnNetworkChange = false
|
||||
this.provider = null
|
||||
this.__isProvider = false
|
||||
this.connected = false
|
||||
this.#listeners = new Map()
|
||||
}
|
||||
|
||||
public async request(args: RequestArguments) {
|
||||
try {
|
||||
args = RequestArguments.parse(args)
|
||||
} catch {
|
||||
throw new ProviderRpcError({
|
||||
code: -32602,
|
||||
message: 'Invalid request arguments',
|
||||
})
|
||||
}
|
||||
|
||||
await waitUntilComplete(document)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { method, params } = args
|
||||
|
||||
const messageChannel = new MessageChannel()
|
||||
|
||||
messageChannel.port1.onmessage = ({ data }) => {
|
||||
try {
|
||||
const message = ProxyMessage.parse(data)
|
||||
|
||||
messageChannel.port1.close()
|
||||
|
||||
// note: method side-effects
|
||||
switch (message.type) {
|
||||
case 'status:proxy:success': {
|
||||
if (
|
||||
(method === 'eth_requestAccounts' ||
|
||||
method === 'eth_accounts') &&
|
||||
!this.connected
|
||||
) {
|
||||
this.#listeners.get('connect')?.({ chainId: '0x1' })
|
||||
this.#listeners.get('connected')?.({ chainId: '0x1' })
|
||||
this.connected = true
|
||||
}
|
||||
|
||||
if (method === 'wallet_switchEthereumChain') {
|
||||
this.#listeners.get('chainChanged')?.(message.data)
|
||||
this.#listeners.get('networkChanged')?.(message.data)
|
||||
}
|
||||
|
||||
resolve(message.data)
|
||||
|
||||
return
|
||||
}
|
||||
case 'status:proxy:error': {
|
||||
// note: for those dApps that make call after having permissions revoked
|
||||
if (
|
||||
message.error.message === 'dApp is not permitted by user' &&
|
||||
this.connected
|
||||
) {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
reject(new ProviderRpcError(message.error))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// we don't reject here because incoming message is not from the proxy
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const providerMessage: ProviderMessage = {
|
||||
type: 'status:provider',
|
||||
data: {
|
||||
method,
|
||||
params,
|
||||
},
|
||||
}
|
||||
|
||||
window.postMessage(providerMessage, window.origin, [messageChannel.port2])
|
||||
})
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public async send(...args: unknown[]): Promise<unknown> {
|
||||
return await this.request({
|
||||
method: args[0] as string,
|
||||
params: args[1] as Record<string, unknown>,
|
||||
})
|
||||
}
|
||||
|
||||
public on(event: Event, handler: (args: unknown) => void): void {
|
||||
this.#listeners.set(event, handler)
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
public async close(): Promise<void> {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
public removeListener(event: Event): void {
|
||||
// note: not all dapps remove these on disconnect
|
||||
if (event === 'close' || event === 'disconnect') {
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
this.#listeners.delete(event)
|
||||
}
|
||||
|
||||
public async enable() {
|
||||
return true
|
||||
}
|
||||
|
||||
private async disconnect() {
|
||||
if (!this.connected) {
|
||||
return
|
||||
}
|
||||
|
||||
this.connected = false
|
||||
|
||||
await this.request({
|
||||
method: 'wallet_revokePermissions',
|
||||
params: [
|
||||
{
|
||||
eth_accounts: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: 'status:provider:disconnect',
|
||||
} satisfies ProviderMessage,
|
||||
window.origin,
|
||||
)
|
||||
|
||||
this.#listeners.get('disconnect')?.()
|
||||
this.#listeners.get('close')?.()
|
||||
this.#listeners.clear()
|
||||
}
|
||||
}
|
||||
|
||||
const provider = new Provider()
|
||||
|
||||
/**
|
||||
* @see https://eips.ethereum.org/EIPS/eip-6963 for spec
|
||||
*/
|
||||
function announceProvider() {
|
||||
const info = {
|
||||
uuid: 'c14d6a7e-14c2-477d-bcb7-ffb732145eae',
|
||||
name: 'Status',
|
||||
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzk1MF8xMjM5NikiPiA8bWFzayBpZD0ibWFzazBfOTUwXzEyMzk2IiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSItMSIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMzIj4gPHBhdGggZD0iTTE2IC0wLjAwMDQ4ODI4MUM0IC0wLjAwMDQ4ODI4MSAwIDMuOTk5NTEgMCAxNS45OTk1QzAgMjcuOTk5NSA0IDMxLjk5OTUgMTYgMzEuOTk5NUMyOCAzMS45OTk1IDMyIDI3Ljk5OTUgMzIgMTUuOTk5NUMzMiAzLjk5OTUxIDI4IC0wLjAwMDQ4ODI4MSAxNiAtMC4wMDA0ODgyODFaIiBmaWxsPSJ3aGl0ZSIvPiA8L21hc2s+IDxnIG1hc2s9InVybCgjbWFzazBfOTUwXzEyMzk2KSI+IDxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2ZfOTUwXzEyMzk2KSI+IDxjaXJjbGUgY3g9IjIzIiBjeT0iOC45OTk1MSIgcj0iMTkiIGZpbGw9IiMxOTkyRDciLz4gPC9nPiA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMV9mXzk1MF8xMjM5NikiPiA8Y2lyY2xlIGN4PSIzMyIgY3k9IjE4Ljk5OTUiIHI9IjE5IiBmaWxsPSIjRjZCMDNDIi8+IDwvZz4gPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjJfZl85NTBfMTIzOTYpIj4gPGNpcmNsZSBjeD0iNSIgY3k9IjMwLjk5OTUiIHI9IjE5IiBmaWxsPSIjRkY3RDQ2Ii8+IDwvZz4gPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjNfZl85NTBfMTIzOTYpIj4gPGNpcmNsZSBjeD0iLTciIGN5PSI4Ljk5OTUxIiByPSIxOSIgZmlsbD0iIzcxNDBGRCIvPiA8L2c+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTguMTI4NCA4LjgwODQzQzE0Ljk1NTEgOC45ODk4NCAxMi42MTIxIDExLjg3NDMgMTIuMzUxNiAxNS4xMzc1QzEyLjM2NzYgMTUuMTMyNiAxMi4zODUzIDE1LjEyNzYgMTIuNDAzIDE1LjEyMjdDMTIuNDIwNyAxNS4xMTc4IDEyLjQzODQgMTUuMTEyOSAxMi40NTQ0IDE1LjEwOEMxMi44NTM1IDE1LjAwNjcgMTMuMjYxNyAxNC45NDUyIDEzLjY3MzEgMTQuOTI0M0MxNC41NjQxIDE0Ljg3NDUgMTUuMjg4NyAxNC45NTE2IDE2LjAxMzIgMTUuMDI4OEMxNi43Mzg3IDE1LjEwNiAxNy40NjQgMTUuMTgzMiAxOC4zNTYyIDE1LjEzMjlDMTguNzY1OCAxNS4xMTEzIDE5LjE3MjIgMTUuMDQ3OSAxOS41Njg4IDE0Ljk0NEMyMS4yNDc2IDE0LjUwODYgMjIuMjEzNSAxMy4zNzQgMjIuMTI4MiAxMS44MTkxQzIyLjAyMzEgOS44ODcwOCAyMC4wNzY5IDguNjk3MzIgMTguMTI4NCA4LjgwODQzWk0xMy44Nzk2IDIzLjE5MDlDMTcuMDUyOSAyMy4wMDk1IDE5LjM5NTkgMjAuMTI1IDE5LjY1NjQgMTYuODYxOEMxOS42MzYzIDE2Ljg2OCAxOS42MTM1IDE2Ljg3NDIgMTkuNTkxNSAxNi44ODAxQzE5LjU3ODQgMTYuODgzNyAxOS41NjU1IDE2Ljg4NzIgMTkuNTUzNiAxNi44OTA1QzE5LjE1NDUgMTYuOTkxOSAxOC43NDYyIDE3LjA1MzQgMTguMzM0OCAxNy4wNzQyQzE3LjQ0MjcgMTcuMTI0OSAxNi43MTczIDE3LjA0NzkgMTUuOTkxOSAxNi45NzA4QzE1LjI2NzQgMTYuODkzOCAxNC41NDI4IDE2LjgxNjkgMTMuNjUxOCAxNi44NjcxQzEzLjI0MjEgMTYuODg4OCAxMi44MzU4IDE2Ljk1MjEgMTIuNDM5MiAxNy4wNTYxQzEwLjc2MDMgMTcuNDkwNyA5Ljc5ODI5IDE4LjYyNTMgOS44Nzk3OSAyMC4xODAyQzkuOTg0OTEgMjIuMTEyMiAxMS45MzExIDIzLjMwMiAxMy44Nzk2IDIzLjE5MDlaIiBmaWxsPSJ3aGl0ZSIvPiA8L2c+IDwvZz4gPGRlZnM+IDxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl85NTBfMTIzOTYiIHg9Ii01LjQzMDI2IiB5PSItMTkuNDMwNyIgd2lkdGg9IjU2Ljg2MDUiIGhlaWdodD0iNTYuODYwNSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPiA8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPiA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPiA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0LjcxNTEzIiByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfOTUwXzEyMzk2Ii8+IDwvZmlsdGVyPiA8ZmlsdGVyIGlkPSJmaWx0ZXIxX2ZfOTUwXzEyMzk2IiB4PSI0LjU2OTc0IiB5PSItOS40MzA3NSIgd2lkdGg9IjU2Ljg2MDUiIGhlaWdodD0iNTYuODYwNSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPiA8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPiA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPiA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0LjcxNTEzIiByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfOTUwXzEyMzk2Ii8+IDwvZmlsdGVyPiA8ZmlsdGVyIGlkPSJmaWx0ZXIyX2ZfOTUwXzEyMzk2IiB4PSItMjMuNDMwMyIgeT0iMi41NjkyNSIgd2lkdGg9IjU2Ljg2MDUiIGhlaWdodD0iNTYuODYwNSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPiA8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPiA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPiA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0LjcxNTEzIiByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfOTUwXzEyMzk2Ii8+IDwvZmlsdGVyPiA8ZmlsdGVyIGlkPSJmaWx0ZXIzX2ZfOTUwXzEyMzk2IiB4PSItMzUuNDMwMyIgeT0iLTE5LjQzMDciIHdpZHRoPSI1Ni44NjA1IiBoZWlnaHQ9IjU2Ljg2MDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4gPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4gPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4gPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNC43MTUxMyIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzk1MF8xMjM5NiIvPiA8L2ZpbHRlcj4gPGNsaXBQYXRoIGlkPSJjbGlwMF85NTBfMTIzOTYiPiA8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IndoaXRlIi8+IDwvY2xpcFBhdGg+IDwvZGVmcz4gPC9zdmc+',
|
||||
rdns: 'app.status',
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('eip6963:announceProvider', {
|
||||
detail: Object.freeze({ info, provider }),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
window.addEventListener('eip6963:requestProvider', () => {
|
||||
announceProvider()
|
||||
})
|
||||
|
||||
announceProvider()
|
||||
|
||||
function injectProvider() {
|
||||
Object.defineProperties(provider, {
|
||||
isStatus: {
|
||||
value: true,
|
||||
writable: false,
|
||||
},
|
||||
isMetaMask: {
|
||||
value: true,
|
||||
writable: false,
|
||||
},
|
||||
_metamask: {
|
||||
value: {
|
||||
isUnlocked: () => new Promise(resolve => resolve(true)),
|
||||
},
|
||||
writable: false,
|
||||
},
|
||||
// isCoinbaseWallet: {
|
||||
// value: true,
|
||||
// writable: false,
|
||||
// },
|
||||
qrUrl: {
|
||||
value: null,
|
||||
writable: false,
|
||||
},
|
||||
autoRefreshOnNetworkChange: {
|
||||
value: false,
|
||||
writable: true,
|
||||
},
|
||||
provider: {
|
||||
value: {},
|
||||
writable: true,
|
||||
},
|
||||
['__isProvider']: {
|
||||
value: true,
|
||||
writable: false,
|
||||
},
|
||||
connected: {
|
||||
value: true,
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
Object.seal(provider)
|
||||
|
||||
Object.defineProperties(window, {
|
||||
ethereum: {
|
||||
get() {
|
||||
return provider
|
||||
},
|
||||
configurable: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// let redefinedProvider: any
|
||||
|
||||
if (window.localStorage.getItem('status:default-wallet') !== 'false') {
|
||||
// redefinedProvider = window.ethereum
|
||||
|
||||
injectProvider()
|
||||
}
|
||||
|
||||
// window.addEventListener('storage', () => {
|
||||
// if (window.localStorage.getItem('status:default-wallet') === 'false') {
|
||||
// window.ethereum = redefinedProvider
|
||||
// }
|
||||
// })
|
||||
|
||||
async function waitUntilComplete(document: Document): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
document.addEventListener('readystatechange', () => {
|
||||
if (document.readyState === 'complete') {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { getFaviconUrl } from '~lib/get-favicon-url'
|
||||
import { ProviderMessage } from '~messages/provider-message'
|
||||
|
||||
import { DesktopClient } from '../lib/desktop-client'
|
||||
|
||||
import type { ProxyMessage } from '~messages/proxy-message'
|
||||
import type { EthersError } from 'ethers'
|
||||
import type { PlasmoCSConfig } from 'plasmo'
|
||||
|
||||
export const config: PlasmoCSConfig = {
|
||||
run_at: 'document_start',
|
||||
}
|
||||
|
||||
const desktopClient = new DesktopClient()
|
||||
|
||||
const handleMessage = async (event: MessageEvent) => {
|
||||
if (event.origin !== window.origin) {
|
||||
return
|
||||
}
|
||||
|
||||
let message: ProviderMessage
|
||||
try {
|
||||
message = ProviderMessage.parse(event.data)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type === 'status:provider:disconnect') {
|
||||
desktopClient.stop()
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type !== 'status:provider') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.ports.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await desktopClient.send({
|
||||
...message.data,
|
||||
name: window.location.hostname,
|
||||
url: window.origin,
|
||||
iconUrl: getFaviconUrl() ?? '',
|
||||
})
|
||||
|
||||
event.ports[0].postMessage({
|
||||
type: 'status:proxy:success',
|
||||
data: response,
|
||||
} satisfies ProxyMessage)
|
||||
} catch (error) {
|
||||
let proxyError = {
|
||||
code: -32603,
|
||||
message: isError(error) ? error.message : 'Internal error',
|
||||
}
|
||||
|
||||
/**
|
||||
* ethers.js library has a custom error detection mechanism.
|
||||
* - Detected errors are stored in the `info` object:
|
||||
* @see https://github.com/ethers-io/ethers.js/blob/72c2182d01afa855d131e82635dca3da063cfb31/src.ts/providers/provider-jsonrpc.ts#L976-L1057
|
||||
* - Undetected errors are stored in the `error` field:
|
||||
* @see https://github.com/ethers-io/ethers.js/blob/72c2182d01afa855d131e82635dca3da063cfb31/src.ts/providers/provider-jsonrpc.ts#L1059
|
||||
*/
|
||||
if (isEthersError(error)) {
|
||||
if (isRpcError(error.error)) {
|
||||
proxyError = error.error
|
||||
} else if (isRpcError(error.info?.error)) {
|
||||
proxyError = error.info.error
|
||||
}
|
||||
}
|
||||
|
||||
const proxyMessage: ProxyMessage = {
|
||||
type: 'status:proxy:error',
|
||||
error: proxyError,
|
||||
}
|
||||
|
||||
event.ports[0].postMessage(proxyMessage)
|
||||
}
|
||||
}
|
||||
|
||||
function isError(error: unknown): error is Error {
|
||||
return !!error && typeof error === 'object' && 'message' in error
|
||||
}
|
||||
|
||||
function isEthersError(error: unknown): error is EthersError & {
|
||||
info?: { error?: { code: number; message: string } }
|
||||
} {
|
||||
return (
|
||||
!!error &&
|
||||
typeof error === 'object' &&
|
||||
'error' in error &&
|
||||
error.error !== null &&
|
||||
typeof error.error === 'object' &&
|
||||
'code' in error.error
|
||||
)
|
||||
}
|
||||
|
||||
function isRpcError(
|
||||
error: unknown,
|
||||
): error is { code: number; message: string } {
|
||||
return (
|
||||
!!error &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
'message' in error
|
||||
)
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage)
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
declare global {
|
||||
interface Navigator {
|
||||
// @see https://github.com/lukewarlow/user-agent-data-types/blob/master/index.d.ts
|
||||
userAgentData?: {
|
||||
platform: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type Platform = 'macos' | 'windows' | 'linux'
|
||||
|
||||
export function detectDesktopOS(): Platform | null {
|
||||
const platform =
|
||||
window.navigator.userAgentData?.platform.toLowerCase() ??
|
||||
window.navigator.platform.toLowerCase()
|
||||
|
||||
if (platform.includes('mac')) return 'macos'
|
||||
if (platform.includes('win')) return 'windows'
|
||||
if (platform.includes('linux')) return 'linux'
|
||||
return null
|
||||
}
|
||||
|
||||
export const useDesktopOS = () => {
|
||||
const [desktopOS, setDesktopOS] = useState<Platform | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// note: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform#examples deprecated but use cases are still valid
|
||||
// note: https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator-platform-dev (e.g. "MacIntel", "Win32", "Linux x86_64", "Linux armv81")
|
||||
setDesktopOS(detectDesktopOS())
|
||||
}, [])
|
||||
|
||||
return desktopOS
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T,
|
||||
): [T, (value: T | ((val: T) => T)) => void] {
|
||||
const [storedValue, setStoredValue] = useState<T>(initialValue)
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
setStoredValue(item ? JSON.parse(item) : initialValue)
|
||||
} catch {
|
||||
setStoredValue(initialValue)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.key === key) {
|
||||
setStoredValue(
|
||||
event.newValue ? JSON.parse(event.newValue) : initialValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange)
|
||||
}
|
||||
}, [key, initialValue])
|
||||
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value
|
||||
|
||||
setStoredValue(valueToStore)
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||
window.dispatchEvent(new Event('storage'))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return [storedValue, setValue]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const useOutsideClick = (
|
||||
ref: React.RefObject<HTMLDivElement>,
|
||||
callback: () => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const path = event.composedPath()
|
||||
if (ref.current && !path.includes(ref.current)) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleClick, true)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick, true)
|
||||
}
|
||||
}, [ref, callback])
|
||||
|
||||
return ref
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { WebSocketProvider } from 'ethers'
|
||||
|
||||
import { config } from '~config'
|
||||
|
||||
import type { RequestArguments } from '~lib/request-arguments'
|
||||
import type { WebSocketLike } from 'ethers'
|
||||
|
||||
type DesktopRequestArguments = RequestArguments & {
|
||||
name: string
|
||||
url: string
|
||||
iconUrl: string
|
||||
}
|
||||
|
||||
export class DesktopClient {
|
||||
#rpcClient: WebSocketProvider | null = null
|
||||
|
||||
public stop() {
|
||||
if (!this.#rpcClient) {
|
||||
return
|
||||
}
|
||||
|
||||
this.#rpcClient?.destroy()
|
||||
this.#rpcClient = null
|
||||
|
||||
// todo: publish disconnect message/event after https://github.com/status-im/status-desktop/issues/16014
|
||||
}
|
||||
|
||||
public async send(args: DesktopRequestArguments) {
|
||||
if (!this.#rpcClient) {
|
||||
this.#rpcClient = new WebSocketProvider(
|
||||
config.desktop.rpc.url,
|
||||
'mainnet',
|
||||
{
|
||||
staticNetwork: true,
|
||||
},
|
||||
)
|
||||
;(this.#rpcClient.websocket as WebSocket).onclose = () => {
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
|
||||
await waitUntilOpen(this.#rpcClient.websocket)
|
||||
|
||||
return await this.#rpcClient.send(config.desktop.rpc.method, [
|
||||
JSON.stringify(args),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilOpen(websocket: WebSocketLike) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (websocket.readyState === WebSocket.OPEN) {
|
||||
resolve()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (websocket.readyState === WebSocket.CLOSING) {
|
||||
reject(new Error('The RPC server is closing'))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (websocket.readyState === WebSocket.CLOSED) {
|
||||
reject(new Error('The RPC server is closed'))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Timed out to connect to the RPC server'))
|
||||
}, 30 * 1000)
|
||||
|
||||
const onopen = websocket.onopen?.bind(websocket)
|
||||
websocket.onopen = event => {
|
||||
onopen?.(event)
|
||||
websocket.onopen = onopen!
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
}
|
||||
|
||||
const onerror = websocket.onerror?.bind(websocket)
|
||||
websocket.onerror = event => {
|
||||
onerror?.(event)
|
||||
websocket.onerror = onerror!
|
||||
clearTimeout(timeout)
|
||||
reject(new Error('Failed to connect to the RPC server'))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function getFaviconUrl() {
|
||||
const faviconElement =
|
||||
document.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
||||
|
||||
if (faviconElement) {
|
||||
return faviconElement.href
|
||||
}
|
||||
|
||||
const iconElements =
|
||||
document.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
|
||||
|
||||
if (iconElements.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return iconElements[0].href
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export class ProviderRpcError extends Error {
|
||||
public code: number
|
||||
public data: unknown
|
||||
|
||||
constructor(args: { code: number; message: string; data?: unknown }) {
|
||||
super(args.message)
|
||||
this.code = args.code
|
||||
this.data = args.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* @see https://eips.ethereum.org/EIPS/eip-1193#request-1
|
||||
*/
|
||||
export const RequestArguments = z.object({
|
||||
method: z.string(),
|
||||
params: z
|
||||
.union([z.array(z.unknown()).readonly(), z.record(z.unknown()).optional()])
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type RequestArguments = z.infer<typeof RequestArguments>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { RequestArguments } from '~lib/request-arguments'
|
||||
|
||||
export const ProviderMessage = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('status:provider'),
|
||||
data: RequestArguments,
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('status:provider:disconnect'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('status:provider:data'),
|
||||
}),
|
||||
])
|
||||
|
||||
export type ProviderMessage = z.infer<typeof ProviderMessage>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const ProxyMessage = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('status:proxy:success'),
|
||||
data: z.unknown(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('status:proxy:error'),
|
||||
error: z.object({
|
||||
code: z.number(),
|
||||
message: z.string(),
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
export type ProxyMessage = z.infer<typeof ProxyMessage>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const ServiceWorkerMessage = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('status:icon:clicked'),
|
||||
}),
|
||||
])
|
||||
|
||||
export type ServiceWorkerMessage = z.infer<typeof ServiceWorkerMessage>
|
||||
@@ -0,0 +1,15 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
font-family: theme(fontFamily.sans);
|
||||
color: theme(colors.neutral.100);
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: theme(fontFamily.sans) !important;
|
||||
font-size: initial !important;
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import * as colors from '@status-im/colors'
|
||||
import { scrollbarWidth } from 'tailwind-scrollbar-utilities'
|
||||
import { fontFamily } from 'tailwindcss/defaultTheme'
|
||||
import plugin from 'tailwindcss/plugin'
|
||||
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
export const screens = {
|
||||
// We simulate the desktop first approach by using min-width 1px above the max-width of the previous breakpoint to match the design breakpoints
|
||||
// Otherwise, we would have to use max-width approach and change the entire codebase styles
|
||||
sm: '431px',
|
||||
md: '641px',
|
||||
'2md': '768px',
|
||||
lg: '869px',
|
||||
xl: '1024px',
|
||||
'2xl': '1281px',
|
||||
'3xl': '1441px',
|
||||
// TODO to be defined by design for pro-users
|
||||
'4xl': '1601px',
|
||||
}
|
||||
|
||||
export default {
|
||||
darkMode: 'media',
|
||||
future: {
|
||||
hoverOnlyWhenSupported: true,
|
||||
},
|
||||
content: [
|
||||
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
// './app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', ...fontFamily.sans],
|
||||
mono: fontFamily.mono,
|
||||
},
|
||||
|
||||
// use <Text /> from @status-im/components or arbitrary values
|
||||
// note: https://tailwindcss.com/docs/font-size#providing-a-default-line-height for `[fontSize, { lineHeight?, letterSpacing?, fontWeight? }]`
|
||||
fontSize: {
|
||||
// note: not in design system
|
||||
240: [
|
||||
'15rem',
|
||||
{
|
||||
lineHeight: '13.25rem',
|
||||
letterSpacing: '-0.6rem',
|
||||
},
|
||||
],
|
||||
// note: not in design system
|
||||
120: [
|
||||
'7.5rem',
|
||||
{
|
||||
lineHeight: '5.25rem',
|
||||
letterSpacing: '-0.1575rem',
|
||||
},
|
||||
],
|
||||
88: [
|
||||
'5.5rem',
|
||||
{
|
||||
lineHeight: '5.25rem',
|
||||
letterSpacing: '-0.1155rem',
|
||||
},
|
||||
],
|
||||
// note: not in design system
|
||||
76: [
|
||||
'4.75rem',
|
||||
{
|
||||
lineHeight: '4.25rem',
|
||||
letterSpacing: '-0.095rem',
|
||||
},
|
||||
],
|
||||
64: [
|
||||
'4rem',
|
||||
{
|
||||
lineHeight: '4.25rem',
|
||||
letterSpacing: '-0.08rem',
|
||||
},
|
||||
],
|
||||
// note: not in design system
|
||||
48: [
|
||||
'3rem',
|
||||
{
|
||||
lineHeight: '3.125rem',
|
||||
letterSpacing: '-0.06rem',
|
||||
},
|
||||
],
|
||||
40: [
|
||||
'2.5rem',
|
||||
{
|
||||
lineHeight: '2.75rem',
|
||||
letterSpacing: '-0.05rem',
|
||||
},
|
||||
],
|
||||
27: [
|
||||
'1.6875rem',
|
||||
{
|
||||
lineHeight: '2rem',
|
||||
letterSpacing: '-0.0354375rem',
|
||||
},
|
||||
],
|
||||
19: [
|
||||
'1.1875rem',
|
||||
{
|
||||
lineHeight: '1.75rem',
|
||||
letterSpacing: '-0.019rem',
|
||||
},
|
||||
],
|
||||
15: [
|
||||
'0.9375rem',
|
||||
{
|
||||
lineHeight: '1.359375rem',
|
||||
letterSpacing: '-0.0084375rem',
|
||||
},
|
||||
],
|
||||
13: [
|
||||
'0.8125rem',
|
||||
{
|
||||
lineHeight: '1.1375rem',
|
||||
letterSpacing: '-0.0024375rem',
|
||||
},
|
||||
],
|
||||
11: [
|
||||
'0.6875rem',
|
||||
{
|
||||
lineHeight: '0.97625rem',
|
||||
letterSpacing: '-0.0034375rem',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
colors: colors,
|
||||
|
||||
fontWeight: {
|
||||
regular: '400',
|
||||
400: '400',
|
||||
medium: '500',
|
||||
500: '500',
|
||||
semibold: '600',
|
||||
600: '600',
|
||||
bold: '700',
|
||||
700: '700',
|
||||
},
|
||||
|
||||
boxShadow: {
|
||||
1: '0px 2px 20px rgba(9, 16, 28, 0.04)',
|
||||
2: '0px 4px 20px rgba(9, 16, 28, 0.08)',
|
||||
3: '0px 8px 30px rgba(9, 16, 28, 0.12);',
|
||||
},
|
||||
|
||||
extend: {
|
||||
spacing: {
|
||||
30: '7.5rem',
|
||||
},
|
||||
|
||||
borderRadius: {
|
||||
'4xl': '2rem',
|
||||
},
|
||||
|
||||
maxWidth: {
|
||||
page: '1504',
|
||||
},
|
||||
|
||||
transitionProperty: {
|
||||
height: 'height',
|
||||
},
|
||||
|
||||
screens,
|
||||
|
||||
keyframes: {
|
||||
heightIn: {
|
||||
from: { height: '0' },
|
||||
// to: { height: 296 },
|
||||
to: { height: 'var(--radix-navigation-menu-viewport-height)' },
|
||||
},
|
||||
heightOut: {
|
||||
from: { height: 'var(--radix-navigation-menu-viewport-height)' },
|
||||
// from: { height: 296 },
|
||||
to: { height: '0' },
|
||||
},
|
||||
slide: {
|
||||
from: {
|
||||
transform: 'translateX(0)',
|
||||
},
|
||||
to: {
|
||||
transform: 'translateX(-100%)',
|
||||
},
|
||||
},
|
||||
slideInFromBottom: {
|
||||
from: {
|
||||
transform: 'translateY(200px)',
|
||||
},
|
||||
to: {
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
},
|
||||
marquee1: {
|
||||
'0%': { transform: 'translateX(0%)' },
|
||||
'100%': { transform: 'translateX(-100%)' },
|
||||
},
|
||||
explanationIn: {
|
||||
'0%': { opacity: '0', transform: 'scale(0.7)' },
|
||||
'80%': { opacity: '0.8', transform: 'scale(1.02)' },
|
||||
'100%': { opacity: '1', transform: 'scale(1)' },
|
||||
},
|
||||
explanationOut: {
|
||||
'0%': { opacity: '1', transform: 'scale(1)' },
|
||||
'100%': { opacity: '0', transform: 'scale(0.7)' },
|
||||
},
|
||||
explanationSlide: {
|
||||
'0%': {
|
||||
transform: 'translateY(100px)',
|
||||
opacity: '0',
|
||||
},
|
||||
'100%': {
|
||||
transform: 'translateY(0px)',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
marquee2: {
|
||||
'0%': { transform: 'translateX(100%)' },
|
||||
'100%': { transform: 'translateX(0%)' },
|
||||
},
|
||||
|
||||
// dialog
|
||||
'overlay-enter': {
|
||||
from: { opacity: '0' },
|
||||
to: { opacity: '1' },
|
||||
},
|
||||
'overlay-exit': {
|
||||
'0%': { opacity: '1' },
|
||||
'100%': { opacity: '0' },
|
||||
},
|
||||
'drawer-enter': {
|
||||
from: { opacity: '0', transform: 'translateY(100%)' },
|
||||
to: { opacity: '1', transform: 'translateY(0)' },
|
||||
},
|
||||
'drawer-exit': {
|
||||
from: { opacity: '1', transform: 'translateY(0)' },
|
||||
to: { opacity: '0', transform: 'translateY(100%)' },
|
||||
},
|
||||
'dialog-enter': {
|
||||
from: {
|
||||
opacity: '0',
|
||||
transform: 'translate(-50%, -48%) scale(0.96)',
|
||||
},
|
||||
to: {
|
||||
opacity: '1',
|
||||
transform: 'translate(-50%, -50%) scale(1)',
|
||||
},
|
||||
},
|
||||
'dialog-exit': {
|
||||
from: { opacity: '1', transform: 'translate(-50%, -50%) scale(1)' },
|
||||
to: { opacity: '0', transform: 'translate(-50%, -48%) scale(0.96)' },
|
||||
},
|
||||
},
|
||||
|
||||
animation: {
|
||||
heightIn: 'heightIn 250ms ease',
|
||||
heightOut: 'heightOut 250ms ease',
|
||||
slide: '45s slide infinite linear',
|
||||
slideInFromBottom: 'slideInFromBottom 0.5s ease',
|
||||
marquee1: 'marquee1 180s linear infinite',
|
||||
marquee2: 'marquee2 180s linear infinite',
|
||||
explanationIn: 'explanationIn 150ms ease',
|
||||
explanationOut: 'explanationIn 150ms ease reverse',
|
||||
explanationSlideUp: 'explanationSlide 180ms ease',
|
||||
explanationSlideDown: 'explanationSlide 180ms ease reverse',
|
||||
},
|
||||
|
||||
backgroundImage: {
|
||||
'gradient-hatching':
|
||||
'repeating-linear-gradient(-75deg, #E7EAEE, #E7EAEE 4px, rgba(161, 171, 188, 0.2) 0px, rgba(161, 171, 188, 0.2) 6px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('tailwindcss-animate'),
|
||||
// add scrollbar utilities before lands in tailwindcss
|
||||
// @see https://github.com/tailwindlabs/tailwindcss/pull/5732
|
||||
scrollbarWidth(),
|
||||
// scrollbarColor(),
|
||||
// scrollbarGutter(),
|
||||
|
||||
plugin(({ matchUtilities }) => {
|
||||
matchUtilities({
|
||||
perspective: value => ({
|
||||
perspective: value,
|
||||
}),
|
||||
})
|
||||
}),
|
||||
],
|
||||
} satisfies Config
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "plasmo/templates/tsconfig.base",
|
||||
"exclude": ["node_modules"],
|
||||
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx", "global.d.ts"],
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"~*": ["./src/*"],
|
||||
},
|
||||
"baseUrl": ".",
|
||||
"lib": ["WebWorker", "dom", "WebWorker.ImportScripts"],
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'**/dist',
|
||||
'**/node_modules',
|
||||
'**/protos',
|
||||
'**/proto',
|
||||
'**/coverage',
|
||||
'**/storybook-static',
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -7,21 +7,22 @@
|
||||
"packages/status-js",
|
||||
"packages/colors",
|
||||
"packages/icons",
|
||||
"packages/components"
|
||||
"packages/components",
|
||||
"apps/connector"
|
||||
]
|
||||
},
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"prepare": "husky install",
|
||||
"test": "turbo run test --filter=@status-im/* -- --run",
|
||||
"dev": "turbo run dev --filter=@status-im/* --parallel",
|
||||
"build": "turbo run build --filter=@status-im/*",
|
||||
"lint": "turbo run lint --filter=@status-im/* --filter=web",
|
||||
"build": "turbo run build --filter=@status-im/* --filter=connector",
|
||||
"lint": "turbo run lint --filter=@status-im/* --filter=connector",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"format": "prettier --ignore-path .gitignore --write .",
|
||||
"format": "prettier --write .",
|
||||
"clean": "turbo run clean && rimraf node_modules",
|
||||
"storybook": "yarn workspace @status-im/components storybook"
|
||||
"storybook": "pnpm --filter @status-im/components storybook"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.0.33",
|
||||
@@ -29,14 +30,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"@status-im/eslint-config": "*",
|
||||
"@status-im/eslint-config": "workspace:*",
|
||||
"@tsconfig/strictest": "^2.0.0",
|
||||
"@types/prettier": "^2.7.2",
|
||||
"eslint": "^9.14.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.0",
|
||||
"patch-package": "^6.5.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rimraf": "^4.4.1",
|
||||
"turbo": "^1.8.8",
|
||||
"typescript": "^5.6.2",
|
||||
@@ -44,10 +49,17 @@
|
||||
"vite-node": "^0.29.8",
|
||||
"vitest": "^0.29.8"
|
||||
},
|
||||
"packageManager": "yarn@1.22.17",
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"lint-staged": {
|
||||
"*.{md,mdx,yml,yaml,json}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"@achingbrain/ssdp@4.0.1": "patches/@achingbrain__ssdp@4.0.1.patch",
|
||||
"@libp2p/bootstrap@9.0.10": "patches/@libp2p__bootstrap@9.0.10.patch",
|
||||
"it-length-prefixed@9.0.3": "patches/it-length-prefixed@9.0.3.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@status-im/eslint-config"]
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
# @status-im/colors
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3c71b8c: update ESLint
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2fad122: use pnpm and update node
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import configs from '@status-im/eslint-config'
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [...configs]
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@status-im/colors",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"license": "MPL-2.0",
|
||||
"files": [
|
||||
"types",
|
||||
@@ -19,25 +19,29 @@
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"sync": "rimraf src && vite-node scripts/sync.ts",
|
||||
"dev": "vite build --watch --mode development",
|
||||
"build:types": "tsc --noEmit false --emitDeclarationOnly",
|
||||
"build": "vite build",
|
||||
"postbuild": "yarn build:types",
|
||||
"postbuild": "pnpm build:types",
|
||||
"#test": "vitest",
|
||||
"typecheck": "tsc",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "yarn lint --fix",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"format": "prettier --write src",
|
||||
"clean": "rimraf dist node_modules .turbo",
|
||||
"prepack": "yarn build"
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"peerDependencies": {},
|
||||
"devDependencies": {
|
||||
"@clack/prompts": "^0.6.3",
|
||||
"colorjs.io": "^0.4.3",
|
||||
"@status-im/eslint-config": "*",
|
||||
"@status-im/eslint-config": "workspace:*",
|
||||
"figma-api": "^1.11.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"vite": "^5.4.3",
|
||||
|
||||
@@ -4,7 +4,7 @@ export const networks = {
|
||||
hermez: 'rgba(235 132 98 / 100%)',
|
||||
optimism: 'rgba(231 110 110 / 100%)',
|
||||
polygon: 'rgba(173 113 243 / 100%)',
|
||||
unknow: 'rgba(238 242 245 / 100%)',
|
||||
unknown: 'rgba(238 242 245 / 100%)',
|
||||
'x-dai': 'rgba(63 192 189 / 100%)',
|
||||
'zk-sync': 'rgba(159 160 254 / 100%)',
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["plugin:tailwindcss/recommended", "@status-im/eslint-config"],
|
||||
"rules": {
|
||||
"tailwindcss/classnames-order": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,93 @@
|
||||
# @status-im/components
|
||||
|
||||
## 1.0.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1942ffb]
|
||||
- @status-im/icons@1.0.3
|
||||
|
||||
## 1.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3c71b8c: update ESLint
|
||||
- Updated dependencies [3c71b8c]
|
||||
- @status-im/colors@1.0.2
|
||||
- @status-im/icons@1.0.2
|
||||
|
||||
## 1.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2fad122: use pnpm and update node
|
||||
- e30555c: fix <DropdownMenu.SubTrigger /> icon; unknown network typo
|
||||
- 3c968a2: add missing directive to `<SegmentedControl />`
|
||||
- Updated dependencies [2fad122]
|
||||
- @status-im/colors@1.0.1
|
||||
- @status-im/icons@1.0.1
|
||||
|
||||
## 1.0.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2c56ff2: adds segmented control to index file
|
||||
|
||||
## 1.0.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b159258: fix `<DropdownMenu.Item />` onSelect prop
|
||||
- 4953fe7: adds segmented control component
|
||||
|
||||
## 1.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e9979fd: updates icon color for missing compound variants
|
||||
- bf14368: add `type="account"` to `<ContextTag />`
|
||||
|
||||
## 1.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7f21132: fix `<Shortcut />`icon prop and symbol centering
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3ad28ea: chore: Remove flex-1 class from button component (#602)
|
||||
|
||||
## 1.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a3f9252: fix `<Button />` icon color for blur variant
|
||||
|
||||
## 1.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8f1980e: Update fontSize in tailwind.config.ts
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 15c9640: Switch icon button from aria button to regular button
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 447cce7: fix `<DropdowMenu.SubTrigger />` expanded state
|
||||
- 1ce026c: add `<DropdownMenu.Label />`
|
||||
- 4e53b3e: Fix tag and tabs
|
||||
- 273db4e: add dark grey dropdown button
|
||||
- 4452f05: fix `<Button />` text alignment
|
||||
- 433558a: Add `<Switch />` and `<DropdownMenu.SwitchItem />`
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import configs, { tailwindcssConfigs } from '@status-im/eslint-config'
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [...configs, ...tailwindcssConfigs]
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@status-im/components",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.12",
|
||||
"license": "MPL-2.0",
|
||||
"sideEffects": [
|
||||
"*.css"
|
||||
@@ -24,10 +24,14 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "vite build --watch --mode development",
|
||||
"build": "vite build",
|
||||
"postbuild": "yarn build:types",
|
||||
"postbuild": "pnpm build:types",
|
||||
"build:types": "tsc --noEmit false --emitDeclarationOnly",
|
||||
"typecheck": "tsc",
|
||||
"lint": "eslint src",
|
||||
@@ -36,7 +40,7 @@
|
||||
"storybook:dev": "storybook dev -p 3001",
|
||||
"storybook:build": "storybook build",
|
||||
"clean": "rimraf node_modules dist .turbo storybook-static",
|
||||
"prepack": "yarn build"
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
@@ -50,9 +54,10 @@
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@status-im/colors": "*",
|
||||
"@status-im/icons": "*",
|
||||
"@status-im/colors": "workspace:*",
|
||||
"@status-im/icons": "workspace:*",
|
||||
"cva": "^1.0.0-beta.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"react-aria-components": "^1.3.3",
|
||||
@@ -61,7 +66,8 @@
|
||||
"zustand": "^4.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@status-im/eslint-config": "*",
|
||||
"@status-im/eslint-config": "workspace:*",
|
||||
"@storybook/addon-actions": "^8.3.0",
|
||||
"@storybook/addon-designs": "^8.0.3",
|
||||
"@storybook/addon-essentials": "^8.3.0",
|
||||
"@storybook/addon-interactions": "^8.3.0",
|
||||
@@ -71,6 +77,8 @@
|
||||
"@storybook/react": "^8.3.0",
|
||||
"@storybook/react-vite": "^8.3.0",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||
|
||||
|
After Width: | Height: | Size: 127 KiB |
@@ -7,7 +7,7 @@
|
||||
const child_process = require('node:child_process')
|
||||
const process = require('node:process')
|
||||
|
||||
const subprocess = child_process.spawn('yarn', ['storybook:dev', '--no-open'], {
|
||||
const subprocess = child_process.spawn('pnpm', ['storybook:dev', '--no-open'], {
|
||||
detached: true,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ const sizesAvatar = {
|
||||
}
|
||||
|
||||
const renderVariant = (variant: AvatarProps['type']) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (props: any) => {
|
||||
const sizes = sizesAvatar[variant]
|
||||
|
||||
@@ -47,7 +48,7 @@ const meta = {
|
||||
},
|
||||
|
||||
render: props => (
|
||||
<div className="grid gap-4 theme-yellow">
|
||||
<div className="grid gap-4">
|
||||
<h1 className="text-19">User Avatar</h1>
|
||||
{renderVariant('user')({
|
||||
...props,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { Fragment } from 'react'
|
||||
|
||||
import { PlaceholderIcon } from '@status-im/icons/20'
|
||||
|
||||
import { Button } from './button'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
@@ -51,7 +55,22 @@ const meta: Meta<
|
||||
).map(variant => (
|
||||
<div key={variant} className="flex items-center gap-4">
|
||||
{(['40', '32', '24'] as const).map(size => (
|
||||
<Button key={size} {...args} variant={variant} size={size} />
|
||||
<Fragment key={size}>
|
||||
<Button {...args} variant={variant} size={size} />
|
||||
{/* With icon */}
|
||||
<Button
|
||||
{...args}
|
||||
size={size}
|
||||
variant={variant}
|
||||
iconBefore={<PlaceholderIcon />}
|
||||
/>
|
||||
<Button
|
||||
aria-label="Button with icon only"
|
||||
size={size}
|
||||
variant={variant}
|
||||
icon={<PlaceholderIcon />}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -14,7 +14,7 @@ type Variants = VariantProps<typeof styles>
|
||||
type Props = {
|
||||
size?: Variants['size']
|
||||
variant?: Variants['variant']
|
||||
onPress?: () => void
|
||||
onPress?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
} & (
|
||||
| {
|
||||
children: React.ReactNode
|
||||
@@ -76,7 +76,7 @@ function Button(
|
||||
{iconBefore}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1 whitespace-nowrap">{children}</span>
|
||||
<span className="whitespace-nowrap">{children}</span>
|
||||
{iconAfter && (
|
||||
<span className={iconStyles({ size, placement: 'after', variant })}>
|
||||
{iconAfter}
|
||||
@@ -143,16 +143,18 @@ const iconStyles = cva({
|
||||
variant: {
|
||||
primary: ['text-blur-white/70'],
|
||||
positive: ['text-blur-white/70'],
|
||||
grey: ['text-neutral-50'],
|
||||
darkGrey: [
|
||||
grey: [
|
||||
'text-neutral-50',
|
||||
'blur:text-neutral-80/40',
|
||||
'dark:text-neutral-40',
|
||||
'blur:dark:text-neutral-80/40',
|
||||
'blur:dark:text-white-40',
|
||||
],
|
||||
darkGrey: ['text-neutral-40'],
|
||||
outline: [
|
||||
'text-neutral-50',
|
||||
'blur:text-neutral-80/40',
|
||||
'dark:text-neutral-40',
|
||||
'blur:dark:text-neutral-80/40',
|
||||
'blur:dark:text-white-40',
|
||||
],
|
||||
ghost: ['text-neutral-50'],
|
||||
danger: ['text-blur-white/70'],
|
||||
@@ -191,11 +193,41 @@ const iconStyles = cva({
|
||||
placement: 'after',
|
||||
className: '-mr-0.5',
|
||||
},
|
||||
{
|
||||
variant: 'grey',
|
||||
iconOnly: true,
|
||||
className: '!text-neutral-100 dark:!text-white-100',
|
||||
},
|
||||
{
|
||||
variant: 'outline',
|
||||
iconOnly: true,
|
||||
className: '!text-neutral-100 dark:!text-white-100',
|
||||
},
|
||||
{
|
||||
variant: 'darkGrey',
|
||||
iconOnly: true,
|
||||
className: '!text-neutral-100 dark:!text-white-100',
|
||||
},
|
||||
{
|
||||
variant: 'ghost',
|
||||
iconOnly: true,
|
||||
className: '!text-neutral-100 dark:!text-white-100',
|
||||
},
|
||||
{
|
||||
variant: 'primary',
|
||||
iconOnly: true,
|
||||
className: '!text-white-100',
|
||||
},
|
||||
{
|
||||
variant: 'positive',
|
||||
iconOnly: true,
|
||||
className: '!text-white-100',
|
||||
},
|
||||
{
|
||||
variant: 'danger',
|
||||
iconOnly: true,
|
||||
className: '!text-white-100',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ const ContextTagVariant = (props: Props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ContextTag {...props} type="account" label="Account name" emoji="🐷" />
|
||||
<ContextTag
|
||||
{...props}
|
||||
type="icon"
|
||||
|
||||
@@ -3,6 +3,8 @@ import { cloneElement, forwardRef } from 'react'
|
||||
import { cva, cx } from 'cva'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
import { Avatar } from '../avatar'
|
||||
|
||||
import type { IconElement } from '../types'
|
||||
import type { VariantProps } from 'cva'
|
||||
|
||||
@@ -15,10 +17,10 @@ type Props = {
|
||||
| { type: 'community'; label: string; icon: IconElement }
|
||||
| { type: 'token'; label: string; icon: IconElement }
|
||||
| { type: 'network'; label: string; icon: IconElement }
|
||||
| { type: 'account'; label: string; emoji: string }
|
||||
| { type: 'icon'; label: string; icon: IconElement }
|
||||
)
|
||||
// | { type: 'user'; user: { name: string; src: string } }
|
||||
// | { type: 'account'; account: { name: string; emoji: string } }
|
||||
// | {
|
||||
// type: 'group'
|
||||
// group: {
|
||||
@@ -36,7 +38,7 @@ type Props = {
|
||||
|
||||
const baseStyles = cva({
|
||||
base: [
|
||||
'inline-flex w-fit cursor-default items-center rounded-full',
|
||||
'inline-flex w-fit cursor-default items-center',
|
||||
'font-medium text-neutral-100 dark:text-white-100',
|
||||
'bg-neutral-10 blur:bg-neutral-80/5 dark:bg-neutral-90 blur:dark:bg-white-5',
|
||||
],
|
||||
@@ -50,6 +52,13 @@ const baseStyles = cva({
|
||||
selected: {
|
||||
true: 'border border-customisation-50',
|
||||
},
|
||||
rounded: {
|
||||
'8': 'rounded-8',
|
||||
full: 'rounded-full',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
rounded: 'full',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -116,6 +125,23 @@ const ContextTag = (props: Props, ref: React.Ref<HTMLDivElement>) => {
|
||||
</div>
|
||||
)
|
||||
})
|
||||
.with({ type: 'account' }, ({ label, emoji }) => {
|
||||
return (
|
||||
<div {...rest} ref={ref} className={baseStyles({ size, rounded: '8' })}>
|
||||
<span className={iconStyles({ size, rounded: '6', offset: size })}>
|
||||
<Avatar
|
||||
type="account"
|
||||
size={match(size)
|
||||
.with('32', () => '28' as const)
|
||||
.otherwise(() => '20' as const)}
|
||||
name={label}
|
||||
emoji={emoji}
|
||||
/>
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
.with({ type: 'icon' }, ({ icon, label }) => {
|
||||
return (
|
||||
<div {...rest} ref={ref} className={baseStyles({ size })}>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { cloneElement, forwardRef, useId } from 'react'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
ExternalIcon,
|
||||
SearchIcon,
|
||||
} from '@status-im/icons/20'
|
||||
@@ -123,7 +123,6 @@ Search.displayName = 'Search'
|
||||
type ItemProps = DropdownMenu.DropdownMenuItemProps & {
|
||||
icon?: IconElement
|
||||
label: string
|
||||
onSelect: () => void
|
||||
selected?: boolean
|
||||
danger?: boolean
|
||||
external?: boolean
|
||||
@@ -302,7 +301,7 @@ export const SubTrigger = (props: SubTriggerProps) => {
|
||||
<span className={iconStyles({ danger })}>{cloneElement(icon)}</span>
|
||||
)}
|
||||
<span className={labelStyles({ danger })}>{label}</span>
|
||||
<ArrowRightIcon className="text-neutral-50" />
|
||||
<ChevronRightIcon className="text-neutral-50" />
|
||||
</DropdownMenu.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BoldIcon } from '@status-im/icons/20'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
|
||||
import { IconButton } from './icon-button'
|
||||
|
||||
@@ -6,11 +7,17 @@ import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const sizes = ['40', '32', '24'] as const
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const renderVariant = (variant: string) => (props: any) => (
|
||||
<div className="flex items-center gap-4">
|
||||
{sizes.map(size => (
|
||||
<IconButton {...props} key={size} variant={variant} icon={BoldIcon} />
|
||||
<IconButton
|
||||
{...props}
|
||||
key={size}
|
||||
variant={variant}
|
||||
icon={<BoldIcon />}
|
||||
onPress={action('press')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -19,7 +26,7 @@ const meta = {
|
||||
component: IconButton,
|
||||
title: 'Components/Icon Button',
|
||||
args: {
|
||||
isDisabled: false,
|
||||
disabled: false,
|
||||
},
|
||||
|
||||
parameters: {
|
||||
|
||||
@@ -3,27 +3,36 @@
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
import { cva } from 'cva'
|
||||
import { Button as AriaButton } from 'react-aria-components'
|
||||
|
||||
import type { IconElement } from '../types'
|
||||
import type { VariantProps } from 'cva'
|
||||
import type { Ref } from 'react'
|
||||
import type { ButtonProps as AriaButtonProps } from 'react-aria-components'
|
||||
|
||||
type Variants = VariantProps<typeof styles>
|
||||
|
||||
type Props = AriaButtonProps & {
|
||||
type Props = {
|
||||
variant?: Variants['variant']
|
||||
icon: IconElement
|
||||
onPress?: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
}
|
||||
|
||||
const IconButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||
const { variant = 'default', icon, ...buttonProps } = props
|
||||
type ButtonProps = Omit<React.ComponentPropsWithoutRef<'button'>, 'children'>
|
||||
|
||||
const IconButton = (
|
||||
props: Props & ButtonProps,
|
||||
ref: Ref<HTMLButtonElement>,
|
||||
) => {
|
||||
const { variant = 'default', onPress: onClick, icon, ...buttonProps } = props
|
||||
|
||||
return (
|
||||
<AriaButton {...buttonProps} ref={ref} className={styles({ variant })}>
|
||||
<button
|
||||
{...buttonProps}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
className={styles({ variant })}
|
||||
>
|
||||
{icon}
|
||||
</AriaButton>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,16 +46,16 @@ const styles = cva({
|
||||
variants: {
|
||||
variant: {
|
||||
default: [
|
||||
'border-transparent bg-neutral-10 text-neutral-50 hover:bg-neutral-20 pressed:border-neutral-20 pressed:bg-neutral-10 pressed:text-neutral-100',
|
||||
'dark:bg-neutral-90 dark:text-neutral-40 dark:hover:bg-neutral-80 dark:pressed:border-neutral-60 dark:pressed:bg-neutral-80/70 dark:pressed:text-white-100',
|
||||
'border-transparent bg-neutral-10 text-neutral-50 active:border-neutral-20 active:bg-neutral-10 active:text-neutral-100 hover:bg-neutral-20',
|
||||
'dark:bg-neutral-90 dark:text-neutral-40 dark:active:border-neutral-60 dark:active:bg-neutral-80/70 dark:active:text-white-100 dark:hover:bg-neutral-80',
|
||||
],
|
||||
outline: [
|
||||
'border-neutral-30 text-neutral-50 hover:border-neutral-40 pressed:border-neutral-20 pressed:bg-neutral-10 pressed:text-neutral-100',
|
||||
'dark:border-neutral-70 dark:text-neutral-40 dark:hover:bg-neutral-60 dark:pressed:border-neutral-60 dark:pressed:bg-neutral-80/70 dark:pressed:text-white-100',
|
||||
'border-neutral-30 text-neutral-50 active:border-neutral-20 active:bg-neutral-10 active:text-neutral-100 hover:border-neutral-40',
|
||||
'dark:border-neutral-70 dark:text-neutral-40 dark:active:border-neutral-60 dark:active:bg-neutral-80/70 dark:active:text-white-100 dark:hover:bg-neutral-60',
|
||||
],
|
||||
ghost: [
|
||||
'border-transparent text-neutral-50 hover:bg-neutral-10 pressed:border-neutral-20 pressed:bg-neutral-10 pressed:text-neutral-100',
|
||||
'dark:text-neutral-40 dark:hover:bg-neutral-80/70 dark:pressed:border-neutral-60 dark:pressed:bg-neutral-80/70 dark:pressed:text-white-100',
|
||||
'border-transparent text-neutral-50 active:border-neutral-20 active:bg-neutral-10 active:text-neutral-100 hover:bg-neutral-10',
|
||||
'dark:text-neutral-40 dark:active:border-neutral-60 dark:active:bg-neutral-80/70 dark:active:text-white-100 dark:hover:bg-neutral-80/70',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ export { IconButton } from './icon-button'
|
||||
export { Input } from './input'
|
||||
export { Popover } from './popover'
|
||||
export { StatusProvider } from './provider'
|
||||
export { SegmentedControl } from './segmented-control'
|
||||
export { Shortcut } from './shortcut'
|
||||
export { Skeleton } from './skeleton'
|
||||
export { Step } from './step'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * as SegmentedControl from './segmented-control'
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { PlaceholderIcon } from '@status-im/icons/20'
|
||||
|
||||
import { SegmentedControl } from './'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
type RootProps = React.ComponentPropsWithoutRef<typeof SegmentedControl.Root>
|
||||
type ItemProps = React.ComponentPropsWithoutRef<typeof SegmentedControl.Item>
|
||||
|
||||
const SegmentedControlVariant = (
|
||||
props: Omit<ItemProps, 'value'> & {
|
||||
count: number
|
||||
size: RootProps['size']
|
||||
variant: RootProps['variant']
|
||||
},
|
||||
) => {
|
||||
const [value, setValue] = useState('0')
|
||||
|
||||
const { count, variant, size, ...itemProps } = props
|
||||
|
||||
return (
|
||||
<SegmentedControl.Root
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
variant={variant}
|
||||
size={size}
|
||||
>
|
||||
{Array(count)
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
return (
|
||||
<SegmentedControl.Item
|
||||
{...(itemProps as ItemProps)}
|
||||
key={index}
|
||||
value={index.toString()}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SegmentedControl.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const SegmentedControlGroup = (
|
||||
props: Omit<RootProps, 'value' | 'onValueChange'>,
|
||||
) => {
|
||||
const { variant = 'grey' } = props
|
||||
|
||||
return (
|
||||
<div className="inline-flex flex-col gap-12">
|
||||
<div className="flex flex-col gap-6">
|
||||
<SegmentedControlVariant count={5} size="32" variant={variant}>
|
||||
Tab
|
||||
</SegmentedControlVariant>
|
||||
|
||||
<SegmentedControlVariant
|
||||
count={5}
|
||||
size="32"
|
||||
variant={variant}
|
||||
icon={<PlaceholderIcon />}
|
||||
>
|
||||
Tab
|
||||
</SegmentedControlVariant>
|
||||
|
||||
<SegmentedControlVariant count={2} size="32" variant={variant}>
|
||||
Tab
|
||||
</SegmentedControlVariant>
|
||||
<SegmentedControlVariant
|
||||
count={2}
|
||||
size="32"
|
||||
variant={variant}
|
||||
icon={<PlaceholderIcon />}
|
||||
>
|
||||
Tab
|
||||
</SegmentedControlVariant>
|
||||
|
||||
<SegmentedControlVariant
|
||||
count={5}
|
||||
size="32"
|
||||
variant={variant}
|
||||
icon={<PlaceholderIcon />}
|
||||
aria-label="placeholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Components/Segmented Control',
|
||||
render: args => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-5">
|
||||
<div className="flex flex-wrap gap-5">
|
||||
<div className="rounded-[24px] bg-white-100 p-12 dark:bg-neutral-90">
|
||||
<SegmentedControlGroup {...args} />
|
||||
</div>
|
||||
|
||||
<div className="inline-flex flex-col gap-12 rounded-[24px] bg-neutral-5 p-12 dark:bg-neutral-95">
|
||||
<SegmentedControlGroup {...args} variant="darkGrey" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-background="blur"
|
||||
className="relative inline-flex flex-col gap-12 overflow-hidden rounded-[24px] p-12"
|
||||
>
|
||||
<div className="absolute left-0 top-0 z-10 size-full bg-blur-white/70 backdrop-blur-[20px] dark:bg-blur-neutral-80/80" />
|
||||
{/* Background image */}
|
||||
<div className="absolute left-0 top-0 size-full bg-[url(./assets/background-blur.png)] bg-cover bg-center bg-no-repeat" />
|
||||
<div className="relative z-10">
|
||||
<SegmentedControlGroup {...args} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj
|
||||
|
||||
export const Light: Story = {}
|
||||
export const Dark: Story = {
|
||||
parameters: {
|
||||
backgrounds: { default: 'dark' },
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
@@ -0,0 +1,212 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
cloneElement,
|
||||
createContext,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import * as ToggleGroup from '@radix-ui/react-toggle-group'
|
||||
import { cva } from 'cva'
|
||||
|
||||
import type { IconElement } from '../types'
|
||||
import type { VariantProps } from 'cva'
|
||||
|
||||
type Variants = VariantProps<typeof rootStyles>
|
||||
|
||||
const SegmentedControlContext = createContext<
|
||||
Pick<Variants, 'variant' | 'size'>
|
||||
>({})
|
||||
|
||||
function useSegmentedControlContext() {
|
||||
const context = useContext(SegmentedControlContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useSegmentedControlContext must be used within a <SegmentedControl.Root />',
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type RootProps = Omit<ToggleGroup.ToggleGroupSingleProps, 'type'> & {
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
variant?: Variants['variant']
|
||||
size?: Variants['size']
|
||||
}
|
||||
|
||||
const rootStyles = cva({
|
||||
base: 'relative flex flex-1 items-center justify-center gap-0.5 rounded-10 p-0.5',
|
||||
variants: {
|
||||
variant: {
|
||||
grey: 'bg-neutral-10 blur:bg-neutral-80/5 blur:backdrop-blur-[20px] dark:bg-neutral-80 blur:dark:bg-white-5',
|
||||
darkGrey: 'bg-neutral-20 dark:bg-neutral-90',
|
||||
},
|
||||
size: {
|
||||
'24': 'h-6',
|
||||
'32': 'h-8',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const activeSegmentStyles = cva({
|
||||
base: 'pointer-events-none absolute inset-y-0.5 left-0 flex-1 rounded-8 transition-all duration-200 ease-out',
|
||||
variants: {
|
||||
variant: {
|
||||
grey: 'bg-neutral-50 blur:bg-neutral-80/60 dark:bg-neutral-60 blur:dark:bg-white-20',
|
||||
darkGrey: 'bg-neutral-50 dark:bg-neutral-60',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const Root = forwardRef<
|
||||
React.ElementRef<typeof ToggleGroup.Root>,
|
||||
RootProps
|
||||
>((props, ref) => {
|
||||
const {
|
||||
children,
|
||||
variant = 'grey',
|
||||
size = '32',
|
||||
value,
|
||||
onValueChange,
|
||||
...rootProps
|
||||
} = props
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
useImperativeHandle(ref, () => rootRef.current!)
|
||||
|
||||
const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({})
|
||||
|
||||
useEffect(() => {
|
||||
const activeButton =
|
||||
rootRef.current!.querySelector<HTMLButtonElement>('[data-state="on"]')!
|
||||
|
||||
if (activeButton) {
|
||||
setIndicatorStyle({
|
||||
width: activeButton.offsetWidth,
|
||||
transform: `translateX(${activeButton.offsetLeft}px)`,
|
||||
})
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<SegmentedControlContext.Provider
|
||||
value={useMemo(() => ({ size, variant }), [size, variant])}
|
||||
>
|
||||
<ToggleGroup.Root
|
||||
{...rootProps}
|
||||
ref={rootRef}
|
||||
type="single"
|
||||
className={rootStyles({ size, variant })}
|
||||
value={value}
|
||||
onValueChange={value => {
|
||||
// Ensuring there is always a value
|
||||
// @see https://www.radix-ui.com/primitives/docs/components/toggle-group#ensuring-there-is-always-a-value
|
||||
if (value) {
|
||||
onValueChange(value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={activeSegmentStyles({ variant })}
|
||||
style={indicatorStyle}
|
||||
/>
|
||||
{children}
|
||||
</ToggleGroup.Root>
|
||||
</SegmentedControlContext.Provider>
|
||||
)
|
||||
})
|
||||
|
||||
Root.displayName = 'Root'
|
||||
|
||||
/**
|
||||
* Item
|
||||
*/
|
||||
|
||||
const itemStyles = cva({
|
||||
base: [
|
||||
'group relative z-10 flex flex-1 select-none items-center justify-center gap-1 whitespace-nowrap rounded-8 bg-transparent font-medium transition-all duration-300 ease-out',
|
||||
'text-neutral-100 data-[state="on"]:text-white-100 dark:text-white-100',
|
||||
],
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
grey: [
|
||||
'data-[state="off"]:hover:bg-neutral-20 data-[state="off"]:blur:hover:bg-neutral-80/5 data-[state="off"]:dark:hover:bg-neutral-70 data-[state="off"]:dark:blur:hover:bg-white-5',
|
||||
],
|
||||
darkGrey: [
|
||||
'data-[state="off"]:hover:bg-neutral-30 data-[state="off"]:dark:hover:bg-neutral-80',
|
||||
],
|
||||
},
|
||||
size: {
|
||||
'24': 'h-6 px-2 text-13',
|
||||
'32': 'h-7 px-3 text-15',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const iconStyles = cva({
|
||||
base: [
|
||||
'size-5 text-neutral-50 group-data-[state="on"]:text-white-100 dark:text-white-40',
|
||||
],
|
||||
variants: {
|
||||
iconOnly: {
|
||||
true: '',
|
||||
false: '-ml-0.5',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type ItemProps = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroup.Item>,
|
||||
'children'
|
||||
> &
|
||||
(
|
||||
| {
|
||||
icon?: IconElement
|
||||
children: React.ReactNode
|
||||
}
|
||||
| {
|
||||
icon: IconElement
|
||||
children?: never
|
||||
'aria-label': string
|
||||
}
|
||||
)
|
||||
|
||||
export const Item = forwardRef<
|
||||
React.ElementRef<typeof ToggleGroup.Item>,
|
||||
ItemProps
|
||||
>((props, ref) => {
|
||||
const { icon, children, ...itemProps } = props
|
||||
|
||||
const { size, variant } = useSegmentedControlContext()
|
||||
|
||||
const iconOnly = children ? false : true
|
||||
|
||||
return (
|
||||
<ToggleGroup.Item
|
||||
{...itemProps}
|
||||
ref={ref}
|
||||
className={itemStyles({ size, variant })}
|
||||
>
|
||||
{icon && (
|
||||
<>
|
||||
{cloneElement(icon, {
|
||||
className: iconStyles({ iconOnly }),
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</ToggleGroup.Item>
|
||||
)
|
||||
})
|
||||
|
||||
Item.displayName = 'Item'
|
||||
@@ -6,10 +6,10 @@ import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const variants = ['primary', 'secondary', 'gray'] as const
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const renderVariant = (variant: (typeof variants)[number]) => (props: any) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Shortcut {...props} variant={variant} icon={CommandIcon} />
|
||||
<Shortcut {...props} variant={variant} icon={<CommandIcon />} />
|
||||
<Shortcut {...props} variant={variant} symbol="K" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { cloneElement, forwardRef } from 'react'
|
||||
|
||||
import { match, P } from 'ts-pattern'
|
||||
|
||||
import { cva } from '../utils/variants'
|
||||
|
||||
import type { IconElement } from '../types'
|
||||
import type { VariantProps } from '../utils/variants'
|
||||
|
||||
const styles = cva({
|
||||
base: 'flex size-4 flex-shrink-0 items-center justify-center rounded-6 border',
|
||||
base: 'grid size-4 flex-shrink-0 place-items-center rounded-6 border',
|
||||
variants: {
|
||||
variant: {
|
||||
primary: [
|
||||
@@ -32,7 +33,7 @@ const styles = cva({
|
||||
type Props = VariantProps<typeof styles> &
|
||||
(
|
||||
| {
|
||||
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
|
||||
icon: IconElement
|
||||
symbol?: never
|
||||
}
|
||||
| {
|
||||
@@ -42,15 +43,17 @@ type Props = VariantProps<typeof styles> &
|
||||
)
|
||||
|
||||
const Shortcut = (props: Props, ref: React.Ref<HTMLDivElement>) => {
|
||||
const { variant = 'primary', ...rest } = props
|
||||
const { variant = 'primary' } = props
|
||||
|
||||
return (
|
||||
<div className={styles({ variant })} ref={ref} {...rest}>
|
||||
<div className={styles({ variant })} ref={ref}>
|
||||
{match(props)
|
||||
.with({ symbol: P.string }, ({ symbol }) => (
|
||||
<span className="text-11 font-medium">{symbol}</span>
|
||||
))
|
||||
.with({ icon: P._ }, ({ icon: Icon }) => <Icon className="size-3" />)
|
||||
.with({ icon: P._ }, ({ icon }) =>
|
||||
cloneElement(icon, { className: 'size-3' }),
|
||||
)
|
||||
.exhaustive()}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { cva, cx, type VariantProps } from 'cva'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const styles = cva({
|
||||
base: 'animate-skeleton overflow-hidden',
|
||||
|
||||
|
||||
@@ -26,6 +26,16 @@ type RootProps = React.ComponentProps<typeof Tabs.Root> & {
|
||||
|
||||
const TabsContext = createContext<Pick<RootProps, 'size' | 'variant'>>({})
|
||||
|
||||
function useTabsContext() {
|
||||
const context = useContext(TabsContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useTabsContext must be used within a <Tabs.Root />')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const Root = (props: RootProps) => {
|
||||
const { size = '32', variant = 'grey', ...rootProps } = props
|
||||
|
||||
@@ -42,7 +52,7 @@ export const List = forwardRef<
|
||||
React.ElementRef<typeof Tabs.List>,
|
||||
React.ComponentPropsWithoutRef<typeof Tabs.List>
|
||||
>((props, ref) => {
|
||||
const { size } = useContext(TabsContext)!
|
||||
const { size } = useTabsContext()
|
||||
|
||||
return (
|
||||
<Tabs.List
|
||||
@@ -74,7 +84,7 @@ export const Trigger = forwardRef<
|
||||
>((props, ref) => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
const { size, variant } = useContext(TabsContext)!
|
||||
const { size, variant } = useTabsContext()
|
||||
|
||||
return (
|
||||
<Tabs.Trigger {...rest} ref={ref} className={tabStyles({ variant, size })}>
|
||||
@@ -101,7 +111,7 @@ Trigger.displayName = Tabs.Trigger.displayName
|
||||
const tabStyles = cva({
|
||||
base: [
|
||||
'group inline-flex items-center gap-1 whitespace-nowrap',
|
||||
'disabled:pointer-events-none disabled:opacity-[.3]',
|
||||
'disabled:opacity-[.3]',
|
||||
],
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { PlaceholderIcon } from '@status-im/icons/20'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
|
||||
import { Tag } from './tag'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta = {
|
||||
const meta: Meta<typeof Tag> = {
|
||||
component: Tag,
|
||||
title: 'Components/Tag',
|
||||
args: {
|
||||
label: 'Tag',
|
||||
disabled: false,
|
||||
selected: false,
|
||||
icon: <PlaceholderIcon />,
|
||||
iconPlacement: 'left',
|
||||
},
|
||||
@@ -24,17 +26,17 @@ const meta = {
|
||||
render: props => (
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<Tag {...props} />
|
||||
<Tag {...props} selected />
|
||||
<Tag {...props} disabled />
|
||||
<Tag {...props} onPress={action('pressed')} selected />
|
||||
<Tag {...props} onPress={action('pressed')} disabled />
|
||||
<Tag {...props} size="24" />
|
||||
<Tag {...props} size="24" selected />
|
||||
<Tag {...props} size="24" disabled />
|
||||
<Tag {...props} size="24" onPress={action('pressed')} selected />
|
||||
<Tag {...props} size="24" onPress={action('pressed')} disabled />
|
||||
<Tag {...props} icon={undefined} />
|
||||
<Tag {...props} label={undefined} />
|
||||
<Tag {...props} iconPlacement="right" />
|
||||
</div>
|
||||
),
|
||||
} satisfies Meta<typeof Tag>
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Tag>
|
||||
|
||||
|
||||
@@ -8,52 +8,77 @@ import type { Ref } from 'react'
|
||||
|
||||
type Variants = VariantProps<typeof styles>
|
||||
|
||||
type Props = React.ComponentProps<'button'> & {
|
||||
type Props = {
|
||||
size?: Variants['size']
|
||||
label?: string
|
||||
icon?: IconElement
|
||||
iconPlacement?: 'left' | 'right'
|
||||
selected?: boolean
|
||||
onPress?: () => void
|
||||
}
|
||||
|
||||
const Tag = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||
const {
|
||||
size = '32',
|
||||
icon,
|
||||
iconPlacement = 'left',
|
||||
label,
|
||||
selected = false,
|
||||
disabled = false,
|
||||
onPress: onClick,
|
||||
...buttonProps
|
||||
} = props
|
||||
type ButtonProps = {
|
||||
onPress: (event: React.MouseEvent<HTMLButtonElement>) => void
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
} & Omit<React.ComponentPropsWithoutRef<'button'>, 'children'>
|
||||
|
||||
type DivProps = Omit<React.ComponentPropsWithoutRef<'div'>, 'children'>
|
||||
|
||||
function Tag(
|
||||
props: Props & (ButtonProps | DivProps),
|
||||
ref: Ref<HTMLButtonElement | HTMLDivElement>,
|
||||
) {
|
||||
const { size = '32', icon, iconPlacement = 'left', label, ...rest } = props
|
||||
|
||||
const iconOnly = Boolean(icon && !label)
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
{...buttonProps}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
data-selected={selected}
|
||||
className={styles({ size, selected, disabled, iconOnly })}
|
||||
>
|
||||
const content = (
|
||||
<>
|
||||
{icon && iconPlacement === 'left' && (
|
||||
<span className={iconStyles({ size, placement: 'left', iconOnly })}>
|
||||
{cloneElement(icon)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{label && <span className="flex-1 whitespace-nowrap">{label}</span>}
|
||||
|
||||
{icon && iconPlacement === 'right' && (
|
||||
<span className={iconStyles({ size, placement: 'right', iconOnly })}>
|
||||
{cloneElement(icon)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
if ('onPress' in props) {
|
||||
const { selected, disabled, ...buttonProps } = props as ButtonProps
|
||||
|
||||
return (
|
||||
<button
|
||||
{...buttonProps}
|
||||
onClick={props.onPress}
|
||||
ref={ref as Ref<HTMLButtonElement>}
|
||||
data-selected={selected ? selected : undefined}
|
||||
className={styles({
|
||||
size,
|
||||
selected,
|
||||
disabled,
|
||||
iconOnly,
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(rest as DivProps)}
|
||||
ref={ref as Ref<HTMLDivElement>}
|
||||
className={styles({
|
||||
size,
|
||||
iconOnly,
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,8 +87,6 @@ const styles = cva({
|
||||
'inline-flex shrink-0 items-center justify-center gap-1 border border-neutral-20 font-medium transition-all hover:border-neutral-30',
|
||||
'outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-customisation-50 focus-visible:ring-offset-2',
|
||||
'disabled:cursor-default disabled:opacity-[.3]',
|
||||
|
||||
// dark
|
||||
'dark:border-neutral-80 dark:text-white-100 dark:hover:border-neutral-60',
|
||||
],
|
||||
variants: {
|
||||
|
||||
@@ -26,7 +26,11 @@ const Actions = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const meta = {
|
||||
/**
|
||||
* > error TS2742: The inferred type of 'meta' cannot be named without a reference to '.pnpm/@storybook+csf@0.1.11/node_modules/@storybook/csf'. This is likely not portable. A type annotation is necessary.
|
||||
* > – @see https://github.com/storybookjs/storybook/issues/24656
|
||||
*/
|
||||
const meta: Meta<typeof Toast> = {
|
||||
component: Toast,
|
||||
title: 'Components/Toast',
|
||||
parameters: {
|
||||
@@ -75,7 +79,7 @@ const meta = {
|
||||
</>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof Toast>
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Toast>
|
||||
|
||||
|
||||
@@ -8,4 +8,4 @@ export type CustomisationColorType = keyof typeof customisation
|
||||
|
||||
export type Prettify<T> = {
|
||||
[K in keyof T]: T[K]
|
||||
} & {} // eslint-disable-line @typescript-eslint/ban-types
|
||||
} & {}
|
||||
|
||||
@@ -34,14 +34,62 @@ export default {
|
||||
},
|
||||
|
||||
fontSize: {
|
||||
88: ['5.5rem', { lineHeight: '5.25rem', letterSpacing: '-0.1155rem' }],
|
||||
64: ['4rem', { lineHeight: '4.25rem', letterSpacing: '-0.08rem' }],
|
||||
40: ['2.5rem', { lineHeight: '2.75rem', letterSpacing: '-0.05rem' }],
|
||||
27: ['1.6875rem', { lineHeight: '2rem', letterSpacing: '0rem' }],
|
||||
19: ['1.1875rem', { lineHeight: '1.75rem', letterSpacing: '0rem' }],
|
||||
15: ['0.9375rem', { lineHeight: '1.36rem', letterSpacing: '0rem' }],
|
||||
13: ['0.8125rem', { lineHeight: '1.1375rem', letterSpacing: '0rem' }],
|
||||
11: ['0.6875rem', { lineHeight: '1', letterSpacing: '0rem' }],
|
||||
88: [
|
||||
'5.5rem',
|
||||
{
|
||||
lineHeight: '5.25rem',
|
||||
letterSpacing: '-0.1155rem',
|
||||
},
|
||||
],
|
||||
64: [
|
||||
'4rem',
|
||||
{
|
||||
lineHeight: '4.25rem',
|
||||
letterSpacing: '-0.08rem',
|
||||
},
|
||||
],
|
||||
40: [
|
||||
'2.5rem',
|
||||
{
|
||||
lineHeight: '2.75rem',
|
||||
letterSpacing: '-0.05rem',
|
||||
},
|
||||
],
|
||||
27: [
|
||||
'1.6875rem',
|
||||
{
|
||||
lineHeight: '2rem',
|
||||
letterSpacing: '-0.0354375rem',
|
||||
},
|
||||
],
|
||||
19: [
|
||||
'1.1875rem',
|
||||
{
|
||||
lineHeight: '1.75rem',
|
||||
letterSpacing: '-0.019rem',
|
||||
},
|
||||
],
|
||||
15: [
|
||||
'0.9375rem',
|
||||
{
|
||||
lineHeight: '1.359375rem',
|
||||
letterSpacing: '-0.0084375rem',
|
||||
},
|
||||
],
|
||||
13: [
|
||||
'0.8125rem',
|
||||
{
|
||||
lineHeight: '1.1375rem',
|
||||
letterSpacing: '-0.0024375rem',
|
||||
},
|
||||
],
|
||||
11: [
|
||||
'0.6875rem',
|
||||
{
|
||||
lineHeight: '0.97625rem',
|
||||
letterSpacing: '-0.0034375rem',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
opacity: {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"ignoreCommand": "git diff --quiet HEAD^ HEAD ../../{patches,package.json,turbo.json} ../../packages/{colors,icons} ./",
|
||||
"installCommand": "yarn install --cwd ../../ --frozen-lockfile",
|
||||
"buildCommand": "turbo run build --cwd ../../ --filter=components... && yarn storybook:build"
|
||||
"installCommand": "pnpm install --dir ../../ --frozen-lockfile",
|
||||
"buildCommand": "turbo run build --cwd ../../ --filter=components... && pnpm storybook:build"
|
||||
}
|
||||
|
||||