Merge branch 'develop' into feature/vesting-delegations

This commit is contained in:
fmtabbara
2022-03-09 15:01:54 +00:00
97 changed files with 35832 additions and 8 deletions
+63
View File
@@ -0,0 +1,63 @@
name: CI for ts-packages
on:
push:
paths:
- 'ts-packages/**'
jobs:
build:
runs-on: custom-runner-linux
steps:
- uses: actions/checkout@v2
- name: Install rsync
run: sudo apt-get install rsync
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Setup yarn
run: npm install -g yarn
- name: Build
run: yarn && yarn build && yarn build:ci
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "ts-packages/dist/storybook/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ts-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Deploy branch to CI www (example)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "ts-packages/dist/example/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ts-${{ env.GITHUB_REF_SLUG }}-example
EXCLUDE: "/dist/, /node_modules/"
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Keybase - Send Notification
env:
NYM_NOTIFICATION_KIND: ts-packages
NYM_PROJECT_NAME: "ts-packages"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
KEYBASE_NYM_CHANNEL: "ci-ts-packages"
IS_SUCCESS: "${{ job.status == 'success' }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
@@ -3,7 +3,7 @@ require('dotenv').config();
const Bot = require('keybase-bot');
let context = {
kinds: ['network-explorer', 'nightly'],
kinds: ['ts-packages', 'network-explorer', 'nightly'],
};
/**
@@ -0,0 +1,29 @@
const Handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
async function addToContextAndValidate(context) {
if (!context.env.NYM_CI_WWW_LOCATION) {
throw new Error('Please ensure the env var NYM_CI_WWW_LOCATION is set');
}
if (!context.env.NYM_CI_WWW_BASE) {
throw new Error('Please ensure the env var NYM_CI_WWW_BASE is set');
}
}
async function getMessageBody(context) {
const source = fs
.readFileSync(
context.env.IS_SUCCESS === 'true'
? path.resolve(__dirname, 'templates', 'success')
: path.resolve(__dirname, 'templates', 'failure'),
)
.toString();
const template = Handlebars.compile(source);
return template(context);
}
module.exports = {
addToContextAndValidate,
getMessageBody,
};
@@ -0,0 +1,11 @@
🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥
> :rocket: {{ env.NYM_PROJECT_NAME }}
> 🔴 **FAILURE** :cry:
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
> `build ` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/actions/runs/{{ env.GITHUB_RUN_ID }}
Commit message:
```
{{ env.GIT_COMMIT_MESSAGE }}
```
@@ -0,0 +1,16 @@
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
> :rocket: {{ env.NYM_PROJECT_NAME }}
> ✅ **SUCCESS**
> ➡️➡️➡️➡️➡️ **View output:**
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}
> `example`: https://{{ env.NYM_CI_WWW_LOCATION }}-example.{{ env.NYM_CI_WWW_BASE }}
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
> `build ` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/actions/runs/{{ env.GITHUB_RUN_ID }}
Commit message by `{{ env.GITHUB_ACTOR }}` at {{ timestamp }}:
```
{{ env.GIT_COMMIT_MESSAGE }}
```
+3 -1
View File
@@ -35,4 +35,6 @@ contracts/mixnet/Justfile
contracts/mixnet/Makefile
validator-config
*.patch
validator-api-config.toml
validator-api-config.toml
dist
storybook-static
+2
View File
@@ -28,6 +28,8 @@ Wallet build instructions are also available on [our docs site](https://nymtech.
There's a `.env.sample-dev` file provided which you can rename to `.env` if you want convenient logging, backtrace, or other environment variables pre-set. The `.env` file is ignored so you don't need to worry about checking it in.
For Typescript components, please see [ts-packages](./ts-packages).
### Developer chat
You can chat to us in [Keybase](https://keybase.io). Download their chat app, then click **Teams -> Join a team**. Type **nymtech.friends** into the team name and hit **continue**. For general chat, hang out in the **#general** channel. Our development takes places in the **#dev** channel. Node operators should be in the **#node-operators** channel.
+12
View File
@@ -0,0 +1,12 @@
# Shared assets
This directory contains asset files shared by many projects in this repo.
You will find:
- favicons
- logos
- shared fonts
- shared icon SVGs
See [ts-packages/react-webpack-with-theme-example](../ts-packages/react-webpack-with-theme-example) for examples of usage.
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg width="300" height="300" viewBox="0 0 296 296" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M148 296C229.738 296 296 229.738 296 148C296 66.2619 229.738 0 148 0C66.2619 0 0 66.2619 0 148C0 229.738 66.2619 296 148 296Z" fill="url(#paint0_linear_113_1244)"/>
<path d="M148 285.875C224.147 285.875 285.875 224.146 285.875 148C285.875 71.8536 224.147 10.1248 148 10.1248C71.8538 10.1248 10.125 71.8536 10.125 148C10.125 224.146 71.8538 285.875 148 285.875Z" fill="#121725"/>
<path d="M88.8829 120.143H88.7169V120.281V168.637L68.3289 120.226L68.3012 120.143H68.1905H56.6272H43.653H43.5146V120.281V175.719V175.857H43.653H56.6272H56.7655V175.719V127.28L77.2365 175.774L77.2642 175.857H77.3748H88.8829H101.829H101.968V175.719V120.281V120.143H101.829H88.8829Z" fill="white"/>
<path d="M252.347 120.143H227.616H227.477L227.45 120.253L214.78 168.858L202.082 120.253L202.054 120.143H201.944H177.157H176.991V120.281V175.719V175.857H177.157H190.104H190.242V175.719V127.667L202.774 175.747L202.801 175.857H202.94H226.564H226.675L226.703 175.747L239.234 127.667V175.719V175.857H239.373H252.347H252.485V175.719V120.281V120.143H252.347Z" fill="white"/>
<path d="M155.663 120.143H155.58L155.552 120.198L139.812 147.557L123.988 120.198L123.96 120.143H123.877H108.911H108.635L108.773 120.364L133.145 162.579V175.719V175.857H133.283H146.257H146.396V175.719V162.579L170.767 120.364L170.905 120.143H170.629H155.663Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_113_1244" x1="0" y1="148" x2="296" y2="148" gradientUnits="userSpaceOnUse">
<stop offset="0.09375" stop-color="#FB6E4E"/>
<stop offset="1" stop-color="#FC1D60"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
@font-face {
font-family: 'Open Sans';
src: url('./OpenSans-VariableFont_wdth,wght.ttf') format('truetype-variations'),
url('./OpenSans-Italic-VariableFont_wdth,wght.ttf') format('truetype-variations');
font-weight: 100 1000;
}
+10
View File
@@ -0,0 +1,10 @@
<svg width="64" height="64" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M40 78.5C61.263 78.5 78.5 61.263 78.5 40C78.5 18.737 61.263 1.5 40 1.5C18.737 1.5 1.5 18.737 1.5 40C1.5 61.263 18.737 78.5 40 78.5Z" fill="#070B15" stroke="url(#paint0_linear_0_1)" stroke-width="3"/>
<path d="M31.4894 27.56L41.8623 56H48.5106H56V24H48.5106V52.4L38.1777 24H31.4894H24V56H31.4894V27.56Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_0_1" x1="0.839161" y1="80" x2="80" y2="80" gradientUnits="userSpaceOnUse">
<stop offset="0.09375" stop-color="#FB6E4E"/>
<stop offset="1" stop-color="#F51473"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 714 B

+13
View File
@@ -0,0 +1,13 @@
<svg width="300" height="300" viewBox="0 0 296 296" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M148 296C229.738 296 296 229.738 296 148C296 66.2619 229.738 0 148 0C66.2619 0 0 66.2619 0 148C0 229.738 66.2619 296 148 296Z" fill="url(#paint0_linear_113_1244)"/>
<path d="M148 285.875C224.147 285.875 285.875 224.146 285.875 148C285.875 71.8536 224.147 10.1248 148 10.1248C71.8538 10.1248 10.125 71.8536 10.125 148C10.125 224.146 71.8538 285.875 148 285.875Z" fill="#121725"/>
<path d="M88.8829 120.143H88.7169V120.281V168.637L68.3289 120.226L68.3012 120.143H68.1905H56.6272H43.653H43.5146V120.281V175.719V175.857H43.653H56.6272H56.7655V175.719V127.28L77.2365 175.774L77.2642 175.857H77.3748H88.8829H101.829H101.968V175.719V120.281V120.143H101.829H88.8829Z" fill="white"/>
<path d="M252.347 120.143H227.616H227.477L227.45 120.253L214.78 168.858L202.082 120.253L202.054 120.143H201.944H177.157H176.991V120.281V175.719V175.857H177.157H190.104H190.242V175.719V127.667L202.774 175.747L202.801 175.857H202.94H226.564H226.675L226.703 175.747L239.234 127.667V175.719V175.857H239.373H252.347H252.485V175.719V120.281V120.143H252.347Z" fill="white"/>
<path d="M155.663 120.143H155.58L155.552 120.198L139.812 147.557L123.988 120.198L123.96 120.143H123.877H108.911H108.635L108.773 120.364L133.145 162.579V175.719V175.857H133.283H146.257H146.396V175.719V162.579L170.767 120.364L170.905 120.143H170.629H155.663Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_113_1244" x1="0" y1="148" x2="296" y2="148" gradientUnits="userSpaceOnUse">
<stop offset="0.09375" stop-color="#FB6E4E"/>
<stop offset="1" stop-color="#FC1D60"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="210" height="56" viewBox="0 0 210 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.8829 0.142822H45.7169V0.28114V48.637L25.3289 0.225818L25.3012 0.142822H25.1905H13.6272H0.652966H0.514648V0.28114V55.7189V55.8572H0.652966H13.6272H13.7655V55.7189V7.28002L34.2365 55.7742L34.2642 55.8572H34.3748H45.8829H58.8294H58.9677V55.7189V0.28114V0.142822H58.8294H45.8829Z"/>
<path d="M209.347 0.142822H184.616H184.477L184.45 0.253483L171.78 48.8583L159.082 0.253483L159.054 0.142822H158.944H134.157H133.991V0.28114V55.7189V55.8572H134.157H147.104H147.242V55.7189V7.66731L159.774 55.7466L159.801 55.8572H159.94H183.564H183.675L183.703 55.7466L196.234 7.66731V55.7189V55.8572H196.373H209.347H209.485V55.7189V0.28114V0.142822H209.347Z"/>
<path d="M112.663 0.142822H112.58L112.552 0.198153L96.8116 27.5574L80.988 0.198153L80.9604 0.142822H80.8774H65.9114H65.6348L65.7731 0.364136L90.1447 42.5787V55.7189V55.8572H90.283H103.257H103.396V55.7189V42.5787L127.767 0.364136L127.905 0.142822H127.629H112.663Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M170.7 29.3001C131.7 -9.7999 68.3001 -9.7999 29.3001 29.3001C-9.7999 68.4001 -9.7999 131.7 29.3001 170.7C68.4001 209.8 131.7 209.8 170.7 170.7C209.8 131.7 209.8 68.3001 170.7 29.3001ZM162.1 162.1C127.8 196.4 72.1001 196.4 37.8001 162.1C3.5001 127.8 3.5001 72.1001 37.8001 37.8001C72.1001 3.5001 127.8 3.5001 162.1 37.8001C196.5 72.2001 196.5 127.8 162.1 162.1Z" fill="white"/>
<path d="M162.1 37.9C127.8 3.60005 72.1002 3.60005 37.8002 37.9C3.50019 72.2 3.50019 127.9 37.8002 162.2C72.1002 196.5 127.8 196.5 162.1 162.2C196.5 127.8 196.5 72.2 162.1 37.9ZM63.0002 170.7C56.8002 167.4 51.1002 163.2 46.1002 158.4V41.7C51.3002 36.7 57.2002 32.5 63.6002 29.1L137 140.9V29.3C143.2 32.6 148.9 36.8 153.9 41.6V158.3C148.7 163.3 142.8 167.5 136.4 170.9L63.0002 59.1V170.7Z" fill="#070B15"/>
<path d="M154 158.3V41.7C148.9 36.9 143.2 32.7 137.1 29.4V140.9L63.5 29C57.1 32.4 51.2 36.6 46 41.6V158.3C51.1 163.1 56.8 167.3 62.9 170.6V59.1L136.5 171C142.9 167.6 148.8 163.3 154 158.3Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M170.7 29.3001C131.7 -9.7999 68.3001 -9.7999 29.3001 29.3001C-9.7999 68.4001 -9.7999 131.7 29.3001 170.7C68.4001 209.8 131.7 209.8 170.7 170.7C209.8 131.7 209.8 68.3001 170.7 29.3001ZM162.1 162.1C127.8 196.4 72.1001 196.4 37.8001 162.1C3.5001 127.8 3.5001 72.1001 37.8001 37.8001C72.1001 3.5001 127.8 3.5001 162.1 37.8001C196.5 72.2001 196.5 127.8 162.1 162.1Z" fill="#141521"/>
<path d="M162.1 37.9C127.8 3.60005 72.1002 3.60005 37.8002 37.9C3.50019 72.2 3.50019 127.9 37.8002 162.2C72.1002 196.5 127.8 196.5 162.1 162.2C196.5 127.8 196.5 72.2 162.1 37.9ZM63.0002 170.7C56.8002 167.4 51.1002 163.2 46.1002 158.4V41.7C51.3002 36.7 57.2002 32.5 63.6002 29.1L137 140.9V29.3C143.2 32.6 148.9 36.8 153.9 41.6V158.3C148.7 163.3 142.8 167.5 136.4 170.9L63.0002 59.1V170.7Z" fill="white"/>
<path d="M154 158.3V41.7C148.9 36.9 143.2 32.7 137.1 29.4V140.9L63.5 29C57.1 32.4 51.2 36.6 46 41.6V158.3C51.1 163.1 56.8 167.3 62.9 170.6V59.1L136.5 171C142.9 167.6 148.8 163.3 154 158.3Z" fill="#141521"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

-6
View File
@@ -89,12 +89,6 @@
"lint": "eslint src",
"lint:fix": "eslint src --fix"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
+6
View File
@@ -0,0 +1,6 @@
{
"packages": [
"ts-packages/*"
],
"version": "0.0.0"
}
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@nymproject/nymsphere",
"version": "1.0.0",
"private": true,
"license": "Apache 2.0",
"workspaces": [
"ts-packages/*",
"nym-wallet",
"explorer"
],
"scripts": {
"build": "lerna run --scope @nymproject/mui-theme --scope @nymproject/react build --stream",
"build:react-example": "lerna run --scope @nymproject/react-webpack-with-theme-example build --stream",
"build:playground": "lerna run --scope @nymproject/react storybook:build --stream",
"build:ci": "yarn build && run-p build:react-example build:playground && yarn build:ci:collect-artifacts",
"build:ci:collect-artifacts": "mkdir -p ts-packages/dist && mv ts-packages/react-components/storybook-static ts-packages/dist/storybook && mv ts-packages/react-webpack-with-theme-example/dist ts-packages/dist/example",
"lint": "lerna run lint --stream",
"lint:fix": "lerna run lint:fix --stream"
},
"devDependencies": {
"lerna": "^4.0.0",
"npm-run-all": "^4.1.5"
}
}
+18
View File
@@ -0,0 +1,18 @@
# See http://editorconfig.org/
# EditorConfig helps developers define and maintain consistent coding styles
# between different editors and IDEs. The EditorConfig project consists of a
# file format for defining coding styles and a collection of text editor plugins
# that enable editors to read the file format and adhere to defined styles.
# EditorConfig files are easily readable and they work nicely with version
# control systems.
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
+6
View File
@@ -0,0 +1,6 @@
{
"root": true,
"extends": [
"@nymproject/eslint-config-react-typescript"
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}
+61
View File
@@ -0,0 +1,61 @@
# Nym Typescript (and Javascript) packages
This directory has shared Typescript (and Javascript) libraries using `yarn workspaces`.
## Why `ts-packages`?
Naming stuff is hard. The `ts-` part means Typescript, because this monorepo also contains Rust crates. So we needed some way to indicate: "put your Typescript here".
Now you know! So, please, put your Typescript here. And your Javascript.
## How does it work?
In the root of this repository is [package.json](../package.json) that specifies an array of globs for packages that are shared:
```
{
"name": "@nymproject/nymsphere",
"version": "1.0.0",
"private": true,
"license": "Apache 2.0",
"workspaces": ["ts-packages/*", "nym-wallet"] <-------
}
```
There are some caveats:
- this only works with `yarn` and not `npm`
- `yarn` creates a single `node_modules` in the root for shared dependencies
- packages that use shared packages, need to be in a path specified in `workspaces`
- local packages take precedence over published packages on `npm`
## Building
From the [root of the repository](../README.md) run:
```
yarn
yarn build
```
This will build all libraries.
Now you can try out [react-webpack-with-theme-example](./react-webpack-with-theme-example) by running:
```
cd ts-packages/react-webpack-with-theme-example
yarn start
```
Our React components have a Storybook in [react-components](./react-components):
```
cd ts-packages/react-components
yarn storybook
```
## Development
Watch mode is available in some packages with `yarn watch`.
See [mui-theme](./mui-theme/README.md) for an example.
@@ -0,0 +1,115 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true,
jest: true,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2019,
sourceType: 'module',
project: './tsconfig.json',
},
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
plugins: ['react', 'react-hooks', 'jsx-a11y', 'prettier', 'jest'],
extends: [
'plugin:react/recommended',
'airbnb',
'airbnb-typescript',
'prettier',
'plugin:jest/recommended',
'plugin:jest/style',
],
ignorePatterns: ['dist/**/*', 'dist', 'node_modules'],
rules: {
'jest/prefer-strict-equal': 'error',
'jest/prefer-to-have-length': 'warn',
'prettier/prettier': 'error',
'import/prefer-default-export': 'off',
'react/prop-types': 'off',
'react/jsx-filename-extension': 'off',
'react/jsx-props-no-spreading': 'off',
'react/require-default-props': [
1, // warn
{
ignoreFunctionalComponents: true,
},
],
// see https://github.com/facebook/create-react-app/discussions/11864#discussioncomment-1933829
// and https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/function-component-definition.md
'react/function-component-definition': [
2, // error
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
'import/no-extraneous-dependencies': [
2, // error
{
devDependencies: [
'**/*.test.[jt]s',
'**/*.spec.[jt]s',
'**/*.test.[jt]sx',
'**/*.spec.[jt]sx',
// see https://github.com/storybookjs/linter-config/blob/master/eslint.config.js#L100
'**/*.stories.*',
'**/.storybook/**/*.*',
],
peerDependencies: true,
},
],
'import/extensions': [
'error',
'ignorePackages',
{
ts: 'never',
tsx: 'never',
js: 'never',
jsx: 'never',
},
],
'@typescript-eslint/no-use-before-define': [
'error',
{ functions: false, classes: true, variables: true, typedefs: true },
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-use-before-define': [0],
'import/no-unresolved': 0,
quotes: 'off',
'@typescript-eslint/quotes': [
2,
'single',
{
avoidEscape: true,
},
],
'@typescript-eslint/no-unused-vars': [2, { argsIgnorePattern: '^_' }],
},
// overrides: [
// {
// files: ['**/*.js', '**/*.jsx'],
// parser: 'espree', // override parser to be the default eslint Javascript parser (see https://eslint.org/docs/user-guide/configuring/plugins#specifying-parser)
// extends: ['plugin:react/recommended', 'airbnb', 'prettier', 'plugin:jest/recommended', 'plugin:jest/style'],
// },
// ],
settings: {
'import/resolver': {
'root-import': {
rootPathPrefix: '@',
rootPathSuffix: 'src',
extensions: ['.js', '.ts', '.tsx', '.jsx', '.mdx'],
},
},
},
};
@@ -0,0 +1,60 @@
{
"name": "@nymproject/eslint-config-react-typescript",
"description": "Import the default eslint config for nym",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "index.js",
"peerDependencies": {
"@typescript-eslint/eslint-plugin": ">= 5",
"@typescript-eslint/parser": ">= 5",
"eslint": ">= 8",
"eslint-config-airbnb": ">= 19",
"eslint-config-airbnb-typescript": ">= 16",
"eslint-config-prettier": ">= 8",
"eslint-import-resolver-root-import": ">= 1",
"eslint-plugin-import": ">= 2",
"eslint-plugin-jest": ">= 26.1.1",
"eslint-plugin-jsx-a11y": ">= 6",
"eslint-plugin-prettier": ">= 4",
"eslint-plugin-react": ">= 7",
"eslint-plugin-react-hooks": ">= 4",
"eslint-plugin-storybook": "*",
"react": "17",
"react-dom": "17"
},
"devDependencies": {
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"prettier": "^2.5.1"
},
"eslintConfig": {
"plugins": [
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:prettier/recommended"
],
"rules": {
"prettier/prettier": "error"
},
"root": true,
"env": {
"browser": false,
"es6": false,
"node": true
}
},
"scripts": {
"lint": "eslint *.js",
"lint:fix": "eslint *.js --fix"
}
}
+70
View File
@@ -0,0 +1,70 @@
# Nym MUI Theme
This package provides an extension to the MUI theme system to use Nym branding.
If you are unfamiliar with Material UI theming, please read the following first:
- https://mui.com/customization/theming/
- https://mui.com/customization/palette/
- https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
## Add theme typings to your project
This package also provides a [template file](./template/mui-theme.d.ts) to add typings to the theme using Typescript's module augmentation.
Read the following if you are unfamiliar with module augmentation and declaration merging. Then
look at the recommendations from Material UI docs for implementation:
- https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
- https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
- https://mui.com/customization/palette/#adding-new-colors
## Example usage
You can see an example of how to use this theme in [react-webpack-with-theme-example](../react-webpack-with-theme-example/src/App.tsx):
```typescript jsx
export const App: React.FC = () => (
<AppContextProvider>
<AppTheme>
<Content />
</AppTheme>
</AppContextProvider>
);
export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext();
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
};
export const Content: React.FC = () => {
...
<Typography sx={{ color: (theme) => theme.palette.nym.networkExplorer.mixnodes.status.active }}>
The quick brown fox jumps over the white fence
</Typography>
...
}
```
## Development
The best way to make changes to the theme is to:
1. Run this package in watch mode with `yarn watch`
2. Run Storybook from [react-components](../react-components/README.md) with `yarn storybook`
3. Make sure the component you are changing is included in the [playground](../react-components/src/playground/index.tsx)
4. Watch for changes in the [Playground story](../react-components/src/stories/Playground.stories.tsx)
Also remember to check light mode and dark mode!
## Building
This package should be built from the root of the repository as follows:
```
yarn
yarn build
```
## Publishing
This package is not published to NPM ... yet.
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@nymproject/mui-theme",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"react": "17",
"react-dom": "17",
"@mui/material": "^5.0.1",
"@mui/styles": "^5.0.1"
},
"devDependencies": {
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"prettier": "^2.5.1",
"typescript": "^4.6.2",
"ts-jest": "^27.0.5",
"jest": "^27.1.0",
"babel-plugin-root-import": "^5.1.0",
"@types/react": "^17.0.34",
"rimraf": "^3.0.2"
},
"scripts": {
"clean": "rimraf dist",
"build": "tsc --noEmit false",
"watch": "tsc --noEmit false -w",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
}
}
+4
View File
@@ -0,0 +1,4 @@
// defines the OpenSans font and processes the font files with Webpack
import '@assets/fonts/fonts.css';
export * from './theme';
+152
View File
@@ -0,0 +1,152 @@
import { PaletteMode } from '@mui/material';
import { PaletteOptions } from '@mui/material/styles';
/**
* This interface defines a palette used across Nym for branding
*/
export interface NymPalette {
highlight: string;
status: {
success: string;
info: string;
};
light: string;
dark: string;
muted: {
onDarkBg: string;
};
}
/**
* This interface defines the palette for a light or dark mode variant
*/
export interface NymPaletteVariant {
mode: PaletteMode;
background: {
main: string;
paper: string;
};
text: {
main: string;
};
topNav: {
background: string;
};
nav: {
text: string;
background: string;
hover: string;
};
mixnodes: {
status: {
active: string;
standby: string;
};
};
}
// -------------------------------------------------------------------------------------------------------------------
/**
* The Nym palette.
*
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode.
*/
export const nymPalette: NymPalette = {
/** emphasises important elements */
highlight: '#FB6E4E',
/** statuses */
status: {
success: '#21D073',
info: '#60D7EF',
},
/** light and dark base values */
light: '#F4F6F8',
dark: '#121726',
/** muted on backgrounds */
muted: {
onDarkBg: '#666B77',
},
};
// -------------------------------------------------------------------------------------------------------------------
/**
* Dark mode variant
*/
export const darkMode: NymPaletteVariant = {
mode: 'dark',
background: {
main: nymPalette.dark,
paper: '#242C3D',
},
text: {
main: '#F2F2F2',
},
topNav: {
background: '#111826',
},
nav: {
text: '#F2F2F2',
background: '#242C3D',
hover: '#111826',
},
mixnodes: {
status: {
active: '#20D073',
standby: '#5FD7EF',
},
},
};
/**
* Light mode variant
*/
export const lightMode: NymPaletteVariant = {
mode: 'light',
background: {
main: '#F2F2F2',
paper: '#FFFFFF',
},
text: {
main: '#121726',
},
topNav: {
background: '#111826',
},
nav: {
text: '#F2F2F2',
background: '#242C3D',
hover: '#111826',
},
mixnodes: {
status: {
active: '#1CBB67',
standby: '#55C1D7',
},
},
};
/**
* Map a Nym palette variant onto the MUI palette
*/
export const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
text: {
primary: variant.text.main,
},
primary: {
main: nymPalette.highlight,
contrastText: '#fff',
},
background: {
default: variant.background.main,
paper: variant.background.paper,
},
});
+22
View File
@@ -0,0 +1,22 @@
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline, PaletteMode } from '@mui/material';
import { getDesignTokens } from './theme';
/**
* Provides the theme for the Nym Components by reacting to the light/dark mode choice.
*
*/
export const NymThemeProvider: React.FC<{ mode: PaletteMode }> = ({ mode, children }) => {
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
);
};
export type { NymPalette, NymPaletteVariant } from './common';
export type { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from './theme';
@@ -0,0 +1,85 @@
import { darkMode, nymPalette, NymPaletteVariant } from './common';
/**
* A palette definition only for the Network Explorer that extends the Nym palette
*/
export interface NetworkExplorerPalette {
networkExplorer: {
map: {
stroke: string;
fills: string[];
};
background: {
tertiary: string;
};
topNav: {
background: string;
socialIcons: string;
appBar: string;
};
nav: {
selected: {
main: string;
nested: string;
};
background: string;
hover: string;
text: string;
};
footer: {
socialIcons: string;
};
mixnodes: {
status: {
active: string;
standby: string;
inactive: string;
};
};
};
}
/**
* Nym palette specific to the Network Explorer
*
* IMPORTANT: do not export this constant, always use the MUI `useTheme` hook to get the correct
* colours for dark/light mode.
*/
export const networkExplorerPalette = (variant: NymPaletteVariant): NetworkExplorerPalette => ({
networkExplorer: {
/** world map styles */
map: {
stroke: '#333333',
fills: ['rgba(255,255,255,0.2)', '#EFEFEF', '#FBE7E1', '#F7D1C6', '#F09379'],
},
background: {
tertiary: variant.mode === 'light' ? '#F4F8FA' : '#323C51',
},
/** left nav styles */
nav: {
selected: {
main: '#111826',
nested: '#3C4558',
},
background: variant.nav.background,
hover: variant.nav.hover,
text: variant.nav.text,
},
topNav: {
...variant.topNav,
appBar: '#080715',
socialIcons: '#F2F2F2',
},
footer: {
socialIcons: variant.mode === 'light' ? nymPalette.muted.onDarkBg : darkMode.text.main,
},
mixnodes: {
status: {
active: variant.mixnodes.status.active,
standby: variant.mixnodes.status.standby,
inactive: variant.text.main,
},
},
},
});
+132
View File
@@ -0,0 +1,132 @@
import { createTheme, Palette, PaletteOptions, ThemeOptions } from '@mui/material/styles';
import { PaletteMode } from '@mui/material';
import { darkMode, lightMode, nymPalette, NymPalette, variantToMUIPalette } from './common';
import { NymWalletPalette, nymWalletPallete } from './wallet';
import { networkExplorerPalette, NetworkExplorerPalette } from './network-explorer';
/**
* To use the theme, copy the file in `../../template/mui-theme.d.ts` into `src/typings/mui-theme.d.ts` in your project.
*
* This will augment the types for `Theme` from `@mui/material/styles` with Nym theme types.
*/
/**
* "Namespace" in MUI palette for Nym that is a union of the base palette and product palettes
*/
export interface NymPaletteWithExtensions {
nym: NymPalette & NymWalletPalette & NetworkExplorerPalette;
}
/**
* Add anything Nym specific to the MUI theme.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface NymTheme {
palette: Palette & NymPaletteWithExtensions;
}
/**
* Type use by MUI's `createTheme` method
*/
export type NymPaletteWithExtensionsOptions = Partial<NymPaletteWithExtensions>;
/**
* Returns the Nym palette for light mode.
*/
const createLightModePalette = (): PaletteOptions & NymPaletteWithExtensionsOptions => ({
nym: {
...nymPalette,
...nymWalletPallete(lightMode),
...networkExplorerPalette(lightMode),
},
...variantToMUIPalette(lightMode),
});
/**
* Returns the Nym palette for dark mode.
*/
const createDarkModePalette = (): PaletteOptions & NymPaletteWithExtensionsOptions => ({
nym: {
...nymPalette,
...nymWalletPallete(darkMode),
...networkExplorerPalette(darkMode),
},
...variantToMUIPalette(darkMode),
});
/**
* Gets the theme options to be passed to `createTheme`.
*
* Based on pattern from https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette.
*
* @param mode The theme mode: 'light' or 'dark'
*/
export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
// first, create the palette from user's choice of light or dark mode
const { palette } = createTheme({
palette: {
mode,
...(mode === 'light' ? createLightModePalette() : createDarkModePalette()),
},
});
// then customise theme and components
return {
typography: {
fontFamily: [
'Open Sans',
'sans-serif',
'BlinkMacSystemFont',
'Roboto',
'Oxygen',
'Ubuntu',
'Helvetica Neue',
].join(','),
fontSize: 14,
fontWeightRegular: 500,
button: {
textTransform: 'none',
fontWeight: '600',
},
},
shape: {
borderRadius: 8,
},
transitions: {
duration: {
shortest: 150,
shorter: 200,
short: 250,
standard: 300,
complex: 375,
enteringScreen: 225,
leavingScreen: 195,
},
easing: {
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
},
},
components: {
MuiButton: {
styleOverrides: {
sizeLarge: {
height: 55,
},
},
},
MuiStepIcon: {
styleOverrides: {
root: {
'&.Mui-completed': {
color: nymPalette.status.success,
},
'&.Mui-active': {
color: nymPalette.dark,
},
},
},
},
},
palette,
};
};
+16
View File
@@ -0,0 +1,16 @@
import { NymPaletteVariant } from './common';
/**
* This interface defines a palette used by the Nym wallet
*/
export interface NymWalletPalette {
wallet: {
fee: string;
};
}
export const nymWalletPallete = (_variant: NymPaletteVariant): NymWalletPalette => ({
wallet: {
fee: '#967FF0',
},
});
+44
View File
@@ -0,0 +1,44 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { PaletteMode } from '@mui/material';
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
import {
NymTheme,
NymPalette,
NymPaletteWithExtensions,
NymPaletteWithExtensionsOptions,
} from '@nymproject/mui-theme';
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist",
"jsx": "react-jsx",
"noEmit": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"dist"
]
}
@@ -0,0 +1,5 @@
{
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}
@@ -0,0 +1,40 @@
/* eslint-disable no-param-reassign */
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/react',
core: {
builder: 'webpack5',
},
// webpackFinal: async (config, { configType }) => {
// // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// // You can change the configuration based on that.
// // 'PRODUCTION' is used when building the static version of storybook.
webpackFinal: async (config) => {
config.module.rules.forEach((rule) => {
// look for SVG import rule and replace
// NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/
if (rule.test?.toString().includes('svg')) {
rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
}
});
// handle asset loading with this
config.module.rules.unshift({
test: /\.svg(\?.*)?$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
});
config.resolve.extensions = ['.tsx', '.ts', '.js'];
config.resolve.plugins = [new TsconfigPathsPlugin()];
// Return the altered config
return config;
},
features: {
emotionAlias: false,
},
};
@@ -0,0 +1,56 @@
/* eslint-disable react/react-in-jsx-scope */
import { NymThemeProvider } from '@nymproject/mui-theme';
import { Box } from '@mui/material';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
const withThemeProvider = (Story, context) => (
<div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}>
<div>
<NymThemeProvider mode="light">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Light mode</h4>
</Box>
</NymThemeProvider>
</div>
<div>
<NymThemeProvider mode="dark">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Dark mode</h4>
</Box>
</NymThemeProvider>
</div>
</div>
);
export const decorators = [withThemeProvider];
+52
View File
@@ -0,0 +1,52 @@
# Nym Shared React Components
This package contains shared React components that are used in other Nym projects.
It uses the following packages:
- [shared MUI theme](../mui-theme/README.md)
- [webpack config](../webpack/README.md)
- [MUI](https://https://mui.com/)
- Typescript
- React
## Building
```
yarn
yarn build
```
## Development
Run watch mode with:
```
yarn watch
```
Or you can run Storybook with:
```
yarn storybook
```
Or you can run the [example project](../react-webpack-with-theme-example/README.md) in dev mode and this package in watch mode, and test results in the example project's dev server output.
## Playground
There are [playground components](./src/playground/index.tsx) that are intended to be used during development to see the effects are changes to the MUI theme or shared components at a glance.
They are available in Storybook from [src/stories/Playground.stories.tsx](./src/stories/Playground.stories.tsx).
> ️ **Tip**: use the playground to make sure components stay consistent and you don't break other components while making changes
## Shared assets
This project uses [shared asset files](../../assets/README.md) such as favicons and logos.
> ️ **Tip**: use to keep your project consistent with Nym's branding and so that it automatically receives changes when the shared assets change. Please try to avoid duplicating the files in the shared assets directory.
## Publishing
This package is not published to NPM ... yet.
+66
View File
@@ -0,0 +1,66 @@
{
"name": "@nymproject/react",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"@mui/material": ">= 5",
"@mui/styles": ">= 5",
"@mui/system": ">= 5",
"@mui/icons-material": ">= 5",
"@mui/lab": "^5.0.0-alpha.72",
"@nymproject/mui-theme": "1",
"react": "17",
"react-dom": "17",
},
"dependencies": {
"flat": "^5.0.2"
},
"devDependencies": {
"@babel/core": "^7.17.5",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@storybook/addon-actions": "^6.4.19",
"@storybook/addon-essentials": "^6.4.19",
"@storybook/addon-interactions": "^6.4.19",
"@storybook/addon-links": "^6.4.19",
"@storybook/builder-webpack5": "^6.4.19",
"@storybook/manager-webpack5": "^6.4.19",
"@storybook/react": "^6.4.19",
"@storybook/testing-library": "^0.0.9",
"@svgr/webpack": "^6.1.1",
"@types/flat": "^5.0.2",
"@types/react": "^17.0.39",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.2.3",
"babel-plugin-root-import": "^5.1.0",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.7",
"jest": "^27.1.0",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
"ts-jest": "^27.0.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2"
},
"scripts": {
"clean": "rimraf dist",
"build": "tsc --noEmit false",
"watch": "tsc --noEmit false -w",
"lint": "eslint src .storybook",
"lint:fix": "eslint src .storybook --fix",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook"
}
}
@@ -0,0 +1 @@
export * from './logo';
@@ -0,0 +1,4 @@
export interface LogoProps {
height?: number | string;
width?: number | string;
}
@@ -0,0 +1,5 @@
import * as React from 'react';
import Logo from '@assets/logo/logo-circle.svg';
import { LogoProps } from './LogoProps';
export const NymLogo: React.FC<LogoProps> = ({ height, width }) => <Logo height={height} width={width} />;
@@ -0,0 +1,9 @@
import * as React from 'react';
import Wordmark from '@assets/logo/logo-wordmark.svg';
import { useTheme } from '@mui/material';
import { LogoProps } from './LogoProps';
export const NymWordmark: React.FC<LogoProps> = ({ height, width }) => {
const theme = useTheme();
return <Wordmark height={height} width={width} fill={theme.palette.text.primary} />;
};
@@ -0,0 +1,2 @@
export * from './NymLogo';
export * from './NymWordmark';
@@ -0,0 +1,31 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { NetworkSelector, Network } from './NetworkSelector';
export default {
title: 'Networks/Network Selector',
component: NetworkSelector,
argTypes: {
network: {
options: ['MAINNET', 'SANDBOX', 'QA'],
control: { type: 'radio' },
},
onSwitchNetwork: { type: 'function' },
},
} as ComponentMeta<typeof NetworkSelector>;
const Template: ComponentStory<typeof NetworkSelector> = ({ network: networkArg, onSwitchNetwork }) => {
const [network, setNetwork] = React.useState<Network | undefined>(networkArg);
const handleClick = (newNetwork?: Network) => {
setNetwork(newNetwork);
if (onSwitchNetwork && newNetwork) {
onSwitchNetwork(newNetwork);
}
};
return <NetworkSelector network={network || networkArg} onSwitchNetwork={handleClick} />;
};
export const Default = Template.bind({});
export const WithValue = Template.bind({});
WithValue.args = { network: 'MAINNET' };
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material';
import { ArrowDropDown, CheckSharp } from '@mui/icons-material';
// TODO: move to sensible shared location
export type Network = 'QA' | 'SANDBOX' | 'MAINNET';
const networks: { networkName: Network; name: string }[] = [
{ networkName: 'MAINNET', name: 'Nym Mainnet' },
{ networkName: 'SANDBOX', name: 'Testnet Sandbox' },
{ networkName: 'QA', name: 'QA' },
];
const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () => void }> = ({
title,
isSelected,
onSelect,
}) => (
<ListItem button onClick={onSelect}>
<ListItemIcon>{isSelected && <CheckSharp color="success" />}</ListItemIcon>
<ListItemText>{title}</ListItemText>
</ListItem>
);
export const NetworkSelector: React.FC<{
network?: Network;
onSwitchNetwork?: (newNetwork: Network) => void;
}> = ({ network, onSwitchNetwork }) => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<>
<Button
variant="text"
color="primary"
sx={{ color: (theme) => `${theme.palette.text.primary}` }}
onClick={handleClick}
disableElevation
endIcon={<ArrowDropDown sx={{ color: (theme) => `1px solid ${theme.palette.primary.main}` }} />}
>
{networks.find((n) => n.networkName === network)?.name}
</Button>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
onClose={handleClose}
>
<List>
<ListSubheader>Network selection</ListSubheader>
{networks
.filter((n) => !(n.networkName === 'QA'))
.map(({ name, networkName }) => (
<NetworkItem
key={networkName}
title={name}
isSelected={networkName === network}
onSelect={() => {
handleClose();
if (onSwitchNetwork) {
onSwitchNetwork(networkName);
}
}}
/>
))}
</List>
</Popover>
</>
);
};
@@ -0,0 +1 @@
export * from './useIsMounted';
@@ -0,0 +1,14 @@
import { useRef, useEffect, useCallback } from 'react';
export function useIsMounted(): () => boolean {
const ref = useRef(false);
useEffect(() => {
ref.current = true;
return () => {
ref.current = false;
};
}, []);
return useCallback(() => ref.current, [ref]);
}
@@ -0,0 +1,3 @@
export * from './components';
export * from './hooks';
export { Playground } from './playground';
@@ -0,0 +1,10 @@
import { Button, Stack } from '@mui/material';
import * as React from 'react';
export const PlaygroundButtons: React.FC = () => (
<Stack spacing={2} direction="row">
<Button variant="text">Text</Button>
<Button variant="contained">Contained</Button>
<Button variant="outlined">Outlined</Button>
</Stack>
);
@@ -0,0 +1,13 @@
import * as React from 'react';
import Checkbox from '@mui/material/Checkbox';
const label = { inputProps: { 'aria-label': 'Checkbox demo' } };
export const PlaygroundCheckboxes: React.FC = () => (
<div>
<Checkbox {...label} defaultChecked />
<Checkbox {...label} />
<Checkbox {...label} disabled />
<Checkbox {...label} disabled checked />
</div>
);
@@ -0,0 +1,19 @@
import * as React from 'react';
const CONTENT = 'The quick brown fox jumped over the white fence';
const WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000];
export const PlaygroundFonts: React.FC = () => (
<div style={{ fontFamily: 'Open Sans' }}>
{WEIGHTS.map((fontWeight) => (
<div key={`weight-${fontWeight}`}>
<div style={{ fontWeight, fontSize: '30px' }}>{CONTENT}</div>
<div>
<code>Font weight: {fontWeight}</code>
</div>
<hr />
</div>
))}
</div>
);
@@ -0,0 +1,25 @@
import * as React from 'react';
import { PlaygroundButtons } from './buttons';
import { PlaygroundCheckboxes } from './checkboxes';
import { PlaygroundBasicSwitches } from './switches';
import { PlaygroundFonts } from './fonts';
import { PlaygroundTheme } from './theme';
export const Playground: React.FC = () => (
<>
<h2>Buttons</h2>
<PlaygroundButtons />
<h2>Checkboxes</h2>
<PlaygroundCheckboxes />
<h2>Switches</h2>
<PlaygroundBasicSwitches />
<h2>Theme</h2>
<PlaygroundTheme />
<h2>Fonts</h2>
<PlaygroundFonts />
</>
);
@@ -0,0 +1,13 @@
import * as React from 'react';
import Switch from '@mui/material/Switch';
const label = { inputProps: { 'aria-label': 'Switch demo' } };
export const PlaygroundBasicSwitches: React.FC = () => (
<div>
<Switch {...label} defaultChecked />
<Switch {...label} />
<Switch {...label} disabled defaultChecked />
<Switch {...label} disabled />
</div>
);
@@ -0,0 +1,21 @@
import * as React from 'react';
import { useTheme } from '@mui/material';
import { MUIThemeExplorer } from './theme/MUIThemeExplorer';
import { PaletteSwatches, PaletteSwatchesList } from './theme/PaletteSwatches';
export const PlaygroundTheme: React.FC = () => {
const theme = useTheme();
return (
<>
<h3>Palette</h3>
<PaletteSwatches theme={theme} />
<h3>Palette Explorer</h3>
<MUIThemeExplorer theme={theme} />
</>
);
};
export const PlaygroundPalette: React.FC = () => {
const theme = useTheme();
return <PaletteSwatchesList theme={theme} />;
};
@@ -0,0 +1,97 @@
/* eslint-disable react/no-array-index-key */
import { Theme } from '@mui/material/styles';
import * as React from 'react';
import TreeView from '@mui/lab/TreeView';
import TreeItem from '@mui/lab/TreeItem';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ArrowRightIcon from '@mui/icons-material/ArrowRight';
import { Box } from '@mui/material';
const MUIThemeExplorerItem: React.FC<{
path: string;
parentKey: string;
theme: Theme;
item: any;
isArrayItem?: boolean;
}> = ({ theme, item, parentKey, path, isArrayItem }) => {
if (!item) {
return null;
}
if (typeof item === 'string') {
return (
<TreeItem
nodeId={`${path}`}
label={
<Box sx={{ display: 'flex', alignItems: 'center', p: 0.5, pr: 0 }}>
<Box
sx={{
mr: 2,
}}
>
<svg height="25px" width="25px">
<rect width="100%" height="100%" fill={item} stroke={theme.palette.text.primary} strokeWidth="1px" />
</svg>
</Box>
{!isArrayItem && <Box mr={2}>{parentKey}</Box>}
<Box mr={2}>
<code>{item}</code>
</Box>
<Box color={theme.palette.text.disabled}>
<code>{path}</code>
</Box>
</Box>
}
/>
);
}
if (Array.isArray(item)) {
return (
<TreeItem nodeId={`${path}-array`} label={`${parentKey}[]`}>
{item.map((i, idx) => (
<MUIThemeExplorerItem
key={`${parentKey}-${idx}`}
parentKey={parentKey}
item={i}
theme={theme}
path={`${path}[${idx}]`}
isArrayItem
/>
))}
</TreeItem>
);
}
if (Object.keys(item).length) {
return (
<TreeItem nodeId={`${path}`} label={`${parentKey}`}>
{Object.keys(item).map((key) => (
<MUIThemeExplorerItem
key={`${parentKey}-${key}`}
parentKey={key}
item={item[key]}
theme={theme}
path={`${path}.${key}`}
/>
))}
</TreeItem>
);
}
return null;
};
export const MUIThemeExplorer: React.FC<{
theme: Theme;
}> = ({ theme }) => (
<TreeView
defaultExpanded={['theme.palette', 'theme.palette.nym', 'theme.palette.nym.highlight']}
defaultCollapseIcon={<ArrowDropDownIcon />}
defaultExpandIcon={<ArrowRightIcon />}
defaultEndIcon={<div style={{ width: 24 }} />}
sx={{ height: 500, flexGrow: 1, width: '100%', overflowY: 'auto' }}
>
<MUIThemeExplorerItem theme={theme} item={theme.palette} parentKey="palette" path="theme.palette" />
</TreeView>
);
@@ -0,0 +1,64 @@
import { Theme } from '@mui/material/styles';
import * as React from 'react';
import { Box, Grid, Typography } from '@mui/material';
import flatten from 'flat';
const SWATCH_SIZE = '40px';
const PaletteSwatch: React.FC<{
theme: Theme;
path: string;
value: string;
width?: string;
}> = ({ theme, path, value, width }) => (
<>
<Box
sx={{
mr: 2,
height: SWATCH_SIZE,
width: SWATCH_SIZE,
background: value,
border: `1px solid ${theme.palette.text.primary}`,
}}
/>
<Box>
<Typography minWidth={width} maxWidth={width} fontFamily="monospace" overflow="scroll" fontSize="12px">
{path}
</Typography>
</Box>
</>
);
export const PaletteSwatches: React.FC<{
theme: Theme;
}> = ({ theme }) => {
const swatches = React.useMemo<any>(() => flatten(theme.palette), [theme.palette]);
return (
<Grid container spacing={2}>
{Object.keys(swatches)
.filter((key) => typeof swatches[key] === 'string' && key !== 'mode')
.map((key) => (
<Grid item>
<PaletteSwatch theme={theme} path={key} value={swatches[key]} width="150px" />
</Grid>
))}
</Grid>
);
};
export const PaletteSwatchesList: React.FC<{
theme: Theme;
}> = ({ theme }) => {
const swatches = React.useMemo<any>(() => flatten(theme.palette), [theme.palette]);
return (
<>
{Object.keys(swatches)
.filter((key) => typeof swatches[key] === 'string' && key !== 'mode')
.map((key) => (
<Box display="flex" alignItems="center" p={1}>
<PaletteSwatch theme={theme} path={key} value={swatches[key]} />
</Box>
))}
</>
);
};
@@ -0,0 +1,10 @@
import { Meta } from '@storybook/addon-docs';
import { NymLogo } from '../components';
<Meta title="Introduction" />
<NymLogo height={100} />
# Nym Design System
This is the Nym design system.
@@ -0,0 +1,12 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { NymLogo, NymWordmark } from '../components';
export default {
title: 'Logo/Nym Logo',
component: NymLogo,
} as ComponentMeta<typeof NymLogo>;
export const Logo = () => <NymLogo height={250} />;
export const Wordmark = () => <NymWordmark height={250} />;
@@ -0,0 +1,20 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { useTheme } from '@mui/material';
import { Playground } from '../playground';
import { PlaygroundPalette } from '../playground/theme';
import { MUIThemeExplorer } from '../playground/theme/MUIThemeExplorer';
export default {
title: 'Playground',
component: Playground,
} as ComponentMeta<typeof Playground>;
export const AllControls = () => <Playground />;
export const Palette = () => <PlaygroundPalette />;
export const ThemeExplorer = () => {
const theme = useTheme();
return <MUIThemeExplorer theme={theme} />;
};
+37
View File
@@ -0,0 +1,37 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
+9
View File
@@ -0,0 +1,9 @@
declare module '*.jpeg' {
const value: any;
export default value;
}
declare module '*.jpg' {
const value: any;
export default value;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.json' {
const content: any;
export default content;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.png' {
const content: any;
export default content;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any;
export default content;
}
@@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"noEmit": true
},
"include": [
".storybook/*.js",
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.stories.*",
],
"exclude": [
"node_modules",
"dist"
]
}
@@ -0,0 +1,19 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"noEmit": true,
"outDir": "./dist",
"declarationDir": "./dist"
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"dist",
"src/stories",
"**/*.stories.*"
]
}
@@ -0,0 +1,3 @@
{
"presets": ["@babel/env", "@babel/react"]
}
@@ -0,0 +1,98 @@
# Example with React + Typescript + Webpack 5 + MUI
An example of using default Webpack and Typescript settings with React and MUI, including theming.
You can use this example as a seed for a new project.
Remember to build the dependency packages from the root of this repo by running:
```
yarn
yarn build
```
If you need to make changes to the dependency packages, you can run `yarn watch` in that package to watch for chagnes and build them. This project will pick up the changes in the built package and hot-reload / recompile.
## Features
### Yarn workspaces
Packages from `ts-packages` are shared using Yarn workspaces. Make sure you add you new project to [package.json](../../package.json) to use the shared packages.
> ⚠️ **Warning**: Yarn workspaces will share all dependencies between projects and works by falling back to parent directories until a `node_modules` directory is found. So be careful when messing around with `node_modules` and resolution, because unexpected things could happen - for example, if you do not run `yarn` from the root and you have a `node_modules` in a directory that is a parent of the directory where you checkout out this repository, that `node_modules` will be used for resolving packages 🙀.
### Typescript
Shared Typescript config is in [tsconfig.json](./tsconfig.json), with specific production settings in [tsconfig.prod.json](./tsconfig.prod.json) that:
- exclude Storybook stories and Jest tests
- do not output typing `*.d.ts` files
### Webpack
Inherit config for Webpack 5 with additional tweaks including:
- favicon generation from [SVG favicon asset files](../../assets/favicon/favicon.svg)
- asset handling (svg, png, fonts, css, etc)
- minification
The development settings include:
- `ts-loader` for quick transpilation
- threaded type checking using `tsc`
- hot reloading using `react-refresh`
### Storybook
Storybook is available in [@nymproject/react](../react-components/src/stories/Introduction.stories.mdx) and can be run using `yarn storybook`.
### MUI and theming
The [Nym theme](../mui-theme/src/theme/theme.ts) provides a theme provider that you can add as follows:
```typescript jsx
export const App: React.FC = () => (
<AppContextProvider>
<AppTheme>
<Content />
</AppTheme>
</AppContextProvider>
);
export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext();
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
};
export const Content: React.FC = () => {
...
}
```
And augment typings for the Theme by adding [mui-theme.d.ts](./src/theme/mui-theme.d.ts):
```typescript
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
declare module '@mui/material/styles' {
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
```
Adding the above, means that any component now has the correct typings, for example, below the Nym palette interface is available for all MUI `Theme` instances with code completion for VSCode and IntelliJ:
```typescript jsx
import { Typography } from '@mui/material';
...
<Typography sx={{ color: (theme) => theme.palette.nym.networkExplorer.mixnodes.status.active }}>
The quick brown fox jumps over the white fence
</Typography>
```
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
@@ -0,0 +1,82 @@
{
"name": "@nymproject/react-webpack-with-theme-example",
"description": "An example project that uses React, Webpack, Typescript and the Nym theme + components library",
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"@mui/material": "^5.0.1",
"@mui/styles": "^5.0.1",
"@mui/icons-material": "^5.5.0",
"@mui/lab": "^5.0.0-alpha.72",
"@nymproject/mui-theme" : "^1.0.0",
"@nymproject/react" : "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^17.0.34",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.2.2",
"babel-plugin-root-import": "^5.1.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv-webpack": "^7.0.3",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.3.2",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"prettier": "^2.5.1",
"react-refresh-typescript": "^2.0.3",
"style-loader": "^3.2.1",
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
},
"scripts": {
"start": "webpack serve --progress --port 3000",
"build": "webpack build --progress --config webpack.prod.js",
"build:dev": "webpack build --progress",
"build:serve": "npx serve dist",
"test": "jest",
"test:watch": "jest --watch",
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
}
}
@@ -0,0 +1,72 @@
import * as React from 'react';
import { Box, Container, Grid, Typography } from '@mui/material';
import { NymLogo, Playground } from '@nymproject/react';
import { NymThemeProvider } from '@nymproject/mui-theme';
import { useTheme } from '@mui/material/styles';
import { ThemeToggle } from './ThemeToggle';
import { AppContextProvider, useAppContext } from './context';
export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext();
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
};
export const Content: React.FC = () => {
const { mode } = useAppContext();
const theme = useTheme();
const swatches: Record<string, string> = {
'palette.primary.main': theme.palette.primary.main,
'palette.secondary.main': theme.palette.secondary.main,
'palette.info.main': theme.palette.info.main,
'palette.success.main': theme.palette.success.main,
'palette.text.primary': theme.palette.text.primary,
'theme.palette.nym.networkExplorer.mixnodes.status.active':
theme.palette.nym.networkExplorer.mixnodes.status.active,
'theme.palette.nym.networkExplorer.mixnodes.status.standby':
theme.palette.nym.networkExplorer.mixnodes.status.standby,
};
return (
<Container sx={{ py: 4 }}>
<Box display="flex" flexDirection="row-reverse" pb={2}>
<ThemeToggle />
</Box>
<NymLogo height={50} />
<h1>Example App</h1>
<Box mb={10}>
<Typography sx={{ color: ({ palette }) => palette.nym.networkExplorer.mixnodes.status.active }}>
This is an example app that uses React, Typescript, Webpack and the Nym theme + components.
</Typography>
<h4>Some colours from the theme (mode = {mode}) are:</h4>
<Grid container spacing={2}>
{Object.keys(swatches).map((key) => (
<Grid item key={key}>
<Box display="flex" alignItems="center">
<svg height="50px" width="50px">
<rect width="100%" height="100%" fill={swatches[key]} />
</svg>
<Typography mx={2}>
<code>{swatches[key]}</code>
<br />
<code>{key}</code>
</Typography>
</Box>
</Grid>
))}
</Grid>
</Box>
<h1>Component playground</h1>
<Playground />
</Container>
);
};
export const App: React.FC = () => (
<AppContextProvider>
<AppTheme>
<Content />
</AppTheme>
</AppContextProvider>
);
@@ -0,0 +1,21 @@
import * as React from 'react';
import { Button, Typography } from '@mui/material';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import { useAppContext } from './context';
export const ThemeToggle: React.FC = () => {
const { mode, toggleMode } = useAppContext();
return (
<Button variant="outlined" color="secondary" onClick={toggleMode} sx={{ display: 'flex', alignItems: 'centre' }}>
{mode === 'dark' ? (
<DarkModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
) : (
<LightModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
)}
<Typography ml={1} color={(theme) => theme.palette.primary.light}>
Switch to {mode === 'dark' ? 'light mode' : 'dark mode'}
</Typography>
</Button>
);
};
@@ -0,0 +1,34 @@
import { PaletteMode } from '@mui/material';
import * as React from 'react';
interface State {
mode: PaletteMode;
toggleMode: () => void;
}
const AppContext = React.createContext<State | undefined>(undefined);
export const useAppContext = (): State => {
const context = React.useContext<State | undefined>(AppContext);
if (!context) {
throw new Error('Please include a `import { AppContextProvider } from "./context"` before using this hook');
}
return context;
};
export const AppContextProvider: React.FC = ({ children }) => {
// light/dark mode
const [mode, setMode] = React.useState<PaletteMode>('dark');
const value = React.useMemo<State>(
() => ({
mode,
toggleMode: () => setMode((prevMode) => (prevMode !== 'light' ? 'light' : 'dark')),
}),
[mode],
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Nym Example with React, Typescript, Webpack</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
</body>
</html>
@@ -0,0 +1,5 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(<App />, document.getElementById('app'));
@@ -0,0 +1,13 @@
import React, { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { App } from '../App';
describe('App', () => {
beforeEach(() => {
render(<App />);
});
it('should render without exploding', () => {
const { container } = render(<App />);
expect(container.firstChild).toBeInTheDocument();
});
});
@@ -0,0 +1,37 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist"
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"build",
"dist"
]
}
@@ -0,0 +1,20 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist",
"declaration": false
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"build",
"dist",
"**/*.stories.*",
"**/*.test.*",
"**/*.spec.*"
]
}
@@ -0,0 +1,18 @@
const path = require('path');
const { mergeWithRules } = require('webpack-merge');
const { webpackCommon } = require('@nymproject/webpack');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(webpackCommon(__dirname), {
entry: path.resolve(__dirname, 'src/index.tsx'),
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
},
});
@@ -0,0 +1,67 @@
const { mergeWithRules } = require('webpack-merge');
const webpack = require('webpack');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
options: {
getCustomTransformers: () => ({
before: [ReactRefreshTypeScript()],
}),
// `ts-loader` does not work with HMR unless `transpileOnly` is used.
// If you need type checking, `ForkTsCheckerWebpackPlugin` is an alternative.
transpileOnly: true,
},
},
],
},
plugins: [
new ReactRefreshWebpackPlugin(),
// this can be included automatically by the dev server, however build mode fails if missing
new webpack.HotModuleReplacementPlugin(),
],
target: 'web',
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
historyApiFallback: true,
},
// recommended for faster rebuild
optimization: {
runtimeChunk: true,
removeAvailableModules: false,
removeEmptyChunks: false,
splitChunks: false,
},
cache: {
type: 'filesystem',
buildDependencies: {
// restart on config change
config: ['./webpack.config.js'],
},
},
});
@@ -0,0 +1,42 @@
const { mergeWithRules } = require('webpack-merge');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const commonConfig = require('./webpack.common');
module.exports = mergeWithRules({
module: {
rules: {
test: 'match',
use: 'replace',
},
},
})(commonConfig, {
mode: 'production',
// TODO: no source maps, add back
devtool: false,
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
optimization: {
minimizer: [`...`, new CssMinimizerPlugin()],
splitChunks: {
chunks: 'all',
},
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
output: {
pathinfo: false,
filename: '[name].[contenthash].js',
},
});
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"compileOnSave": false,
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": false,
"declaration": true,
"declarationMap": true,
"jsx": "react-jsx",
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@assets/*": ["../assets/*"]
},
},
"exclude": [
"jest.config.js",
"webpack.config.js",
"webpack.prod.js",
"webpack.common.js",
"node_modules",
"**/node_modules",
"dist",
"**/dist",
"scripts",
"jest",
"__tests__",
"**/__tests__",
"__jest__",
"**/__jest__",
"config/*"
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"root": true,
"plugins": ["prettier"],
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"rules": {
"prettier/prettier": "error"
},
"env": {
"browser": false,
"es6": true,
"node": true
}
}
+11
View File
@@ -0,0 +1,11 @@
# Common Webpack 5 Config
This package contains shared configuration for webpack:
- [webpack.common.js](./webpack.common.js): common settings for a Typescript + React project
- [webpack.dev.js](./webpack.dev.js): additional settings for dev mode
- [webpack.prod.js](./webpack.prod.js): production settings
## Usage
See [the example](../react-webpack-with-theme-example/README.md)
+9
View File
@@ -0,0 +1,9 @@
const webpackDevConfig = require('./webpack.dev');
const webpackProdConfig = require('./webpack.prod');
const webpackCommon = require('./webpack.common');
module.exports = {
webpackCommon,
webpackDevConfig,
webpackProdConfig,
};
+44
View File
@@ -0,0 +1,44 @@
{
"name": "@nymproject/webpack",
"description": "Provides default Webpack 5 config and dependencies",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "index.js",
"peerDependencies": {
"react": "17",
"react-dom": "17"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@svgr/webpack": "^6.1.1",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0",
"dotenv-webpack": "^7.0.3",
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.3.2",
"prettier": "^2.5.1",
"style-loader": "^3.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"url-loader": "^4.1.1",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
},
"scripts": {
"lint": "eslint *.js",
"lint:fix": "eslint *.js --fix"
}
}
+82
View File
@@ -0,0 +1,82 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const WebpackFavicons = require('webpack-favicons');
const Dotenv = require('dotenv-webpack');
const path = require('path');
/**
* Creates the default Webpack config
* @param baseDir The base directory path, e.g. pass `__dirname` of the webpack config file using this method
*/
module.exports = (baseDir) => ({
module: {
rules: [
{
test: /\.tsx?$/,
use: [{ loader: 'ts-loader', options: { transpileOnly: true } }],
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
},
{
test: /\.(png|jpe?g|gif|md)$/i,
// More information here https://webpack.js.org/guides/asset-modules/
type: 'asset',
},
{
// See https://webpack.js.org/guides/asset-management/#loading-fonts
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.ya?ml$/,
type: 'json',
use: 'yaml-loader',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
plugins: [new TsconfigPathsPlugin()],
alias: {
'react/jsx-runtime': require.resolve('react/jsx-runtime'),
},
},
plugins: [
// new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(baseDir, 'src/index.html'),
}),
new ForkTsCheckerWebpackPlugin({
typescript: {
mode: 'write-references',
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}),
new WebpackFavicons({
src: path.resolve(__dirname, '../../assets/favicon/favicon.svg'), // the asset directory is relative to THIS file
}),
new Dotenv(),
],
output: {
path: path.resolve(baseDir, 'dist'),
publicPath: '/',
},
});
+22
View File
@@ -0,0 +1,22 @@
const { merge } = require('webpack-merge');
const path = require('path');
const common = require('./webpack.common');
/**
* Creates the default Webpack dev config
* @param baseDir The base directory path, e.g. pass `__dirname` of the webpack config file using this method
*/
module.exports = (baseDir) =>
merge(common, {
mode: 'development',
entry: path.resolve(baseDir, '/src/index'),
devServer: {
port: 9000,
compress: true,
historyApiFallback: true,
hot: true,
client: {
overlay: false,
},
},
});
+25
View File
@@ -0,0 +1,25 @@
const path = require('path');
const { default: merge } = require('webpack-merge');
const common = require('./webpack.common');
/**
* Creates the default Webpack prod config
* @param baseDir The base directory path, e.g. pass `__dirname` of the webpack config file using this method
*/
module.exports = (baseDir) =>
merge(common, {
mode: 'production',
node: {
__dirname: false,
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [{ loader: 'ts-loader', options: { transpileOnly: true, configFile: 'tsconfig.prod.json' } }],
exclude: [/node_modules/, '**/*.stories.*', '**/*.test.*'],
},
],
},
entry: path.resolve(baseDir, 'src/index'),
});
+18703
View File
File diff suppressed because it is too large Load Diff