Merge branch 'develop' into feature/vesting-delegations
@@ -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 }}
|
||||
```
|
||||
@@ -35,4 +35,6 @@ contracts/mixnet/Justfile
|
||||
contracts/mixnet/Makefile
|
||||
validator-config
|
||||
*.patch
|
||||
validator-api-config.toml
|
||||
validator-api-config.toml
|
||||
dist
|
||||
storybook-static
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 48 KiB |
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -89,12 +89,6 @@
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"packages": [
|
||||
"ts-packages/*"
|
||||
],
|
||||
"version": "0.0.0"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// defines the OpenSans font and processes the font files with Webpack
|
||||
import '@assets/fonts/fonts.css';
|
||||
|
||||
export * from './theme';
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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.
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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,9 @@
|
||||
declare module '*.jpeg' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module '*.json' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module '*.png' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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/*"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,9 @@
|
||||
const webpackDevConfig = require('./webpack.dev');
|
||||
const webpackProdConfig = require('./webpack.prod');
|
||||
const webpackCommon = require('./webpack.common');
|
||||
|
||||
module.exports = {
|
||||
webpackCommon,
|
||||
webpackDevConfig,
|
||||
webpackProdConfig,
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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: '/',
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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'),
|
||||
});
|
||||