gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: first banner implementation w


From: gnunet
Subject: [taler-wallet-core] branch master updated: first banner implementation with mui
Date: Wed, 09 Mar 2022 18:01:02 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 1607c728 first banner implementation with mui
1607c728 is described below

commit 1607c728bca19a003ca08b64b4d2afc73e4d1e2a
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Mar 9 14:00:02 2022 -0300

    first banner implementation with mui
---
 .../.storybook/preview.js                          |   5 +
 packages/taler-wallet-webextension/package.json    |   7 +-
 .../src/NavigationBar.tsx                          |   5 +-
 .../src/components/Banner.tsx                      |  41 ++
 .../taler-wallet-webextension/src/mui/Avatar.tsx   |   5 +
 .../taler-wallet-webextension/src/mui/Button.tsx   | 215 +++++++
 .../taler-wallet-webextension/src/mui/Divider.tsx  |   5 +
 .../taler-wallet-webextension/src/mui/Grid.tsx     |  13 +
 .../src/mui/Paper.stories.tsx                      | 149 +++++
 .../taler-wallet-webextension/src/mui/Paper.tsx    |  63 ++
 .../src/mui/Typography.tsx                         |   9 +
 .../src/mui/colors/constants.ts                    | 348 +++++++++++
 .../src/mui/colors/manipulation.test.ts            | 305 +++++++++
 .../src/mui/colors/manipulation.ts                 | 273 ++++++++
 .../taler-wallet-webextension/src/mui/style.tsx    | 696 +++++++++++++++++++++
 .../src/popup/DeveloperPage.stories.tsx            |   1 +
 .../src/wallet/ReserveCreated.stories.tsx          |   2 -
 .../src/wallet/ReserveCreated.tsx                  |   1 -
 .../src/wallet/Welcome.tsx                         |   1 -
 pnpm-lock.yaml                                     |  55 +-
 20 files changed, 2186 insertions(+), 13 deletions(-)

diff --git a/packages/taler-wallet-webextension/.storybook/preview.js 
b/packages/taler-wallet-webextension/.storybook/preview.js
index 61484b66..9c1365d3 100644
--- a/packages/taler-wallet-webextension/.storybook/preview.js
+++ b/packages/taler-wallet-webextension/.storybook/preview.js
@@ -128,6 +128,11 @@ export const decorators = [
         <Story />
       </div>
     }
+    if (kind.startsWith('mui')) {
+      return <div style={{ display: 'flex', flexWrap: 'wrap' }}>
+        <Story />
+      </div>
+    }
     if (kind.startsWith('wallet')) {
       const path = /wallet(\/.*).*/.exec(kind)[1];
       return <div class="wallet-container">
diff --git a/packages/taler-wallet-webextension/package.json 
b/packages/taler-wallet-webextension/package.json
index 6641f7dc..c64f7f09 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -33,10 +33,13 @@
     "tslib": "^2.3.1"
   },
   "devDependencies": {
-    "@gnu-taler/pogen": "workspace:*",
     "@babel/core": "7.13.16",
     "@babel/plugin-transform-react-jsx-source": "^7.12.13",
     "@babel/preset-typescript": "^7.13.0",
+    "@gnu-taler/pogen": "workspace:*",
+    "@types/chai": "^4.3.0",
+    "chai": "^4.3.6",
+    "polished": "^4.1.4",
     "@linaria/babel-preset": "3.0.0-beta.4",
     "@linaria/core": "3.0.0-beta.4",
     "@linaria/react": "3.0.0-beta.4",
@@ -81,4 +84,4 @@
   "pogen": {
     "domain": "taler-wallet-webex"
   }
-}
+}
\ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx 
b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index f72d54ef..14619473 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -61,9 +61,6 @@ export enum Pages {
 }
 
 export function PopupNavBar({ path = "" }: { path?: string }): VNode {
-  const innerUrl = chrome.runtime
-    ? new URL(chrome.runtime.getURL("/static/wallet.html#/settings")).href
-    : "#";
   return (
     <NavigationHeader>
       <a href="/balance" class={path.startsWith("/balance") ? "active" : ""}>
@@ -73,7 +70,7 @@ export function PopupNavBar({ path = "" }: { path?: string 
}): VNode {
         <i18n.Translate>Backup</i18n.Translate>
       </a>
       <a />
-      <a href={innerUrl} target="_blank" rel="noreferrer">
+      <a href="/settings">
         <div class="settings-icon" title={i18n.str`Settings`} />
       </a>
     </NavigationHeader>
diff --git a/packages/taler-wallet-webextension/src/components/Banner.tsx 
b/packages/taler-wallet-webextension/src/components/Banner.tsx
new file mode 100644
index 00000000..6ff7b101
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Banner.tsx
@@ -0,0 +1,41 @@
+import { h, Fragment, VNode } from "preact";
+import { Divider } from "../mui/Divider";
+import { Button } from "./styled/index.js";
+import { Typography } from "../mui/Typography";
+import { Avatar } from "../mui/Avatar";
+import { Grid } from "../mui/Grid";
+import { Paper } from "../mui/Paper";
+
+function SignalWifiOffIcon(): VNode {
+  return <Fragment />;
+}
+
+function Banner({}: {}) {
+  return (
+    <Fragment>
+      <Paper elevation={0} /*className={classes.paper}*/>
+        <Grid container wrap="nowrap" spacing={16} alignItems="center">
+          <Grid item>
+            <Avatar /*className={classes.avatar}*/>
+              <SignalWifiOffIcon />
+            </Avatar>
+          </Grid>
+          <Grid item>
+            <Typography>
+              You have lost connection to the internet. This app is offline.
+            </Typography>
+          </Grid>
+        </Grid>
+        <Grid container justify="flex-end" spacing={8}>
+          <Grid item>
+            <Button color="primary">Turn on wifi</Button>
+          </Grid>
+        </Grid>
+      </Paper>
+      <Divider />
+      {/* <CssBaseline /> */}
+    </Fragment>
+  );
+}
+
+export default Banner;
diff --git a/packages/taler-wallet-webextension/src/mui/Avatar.tsx 
b/packages/taler-wallet-webextension/src/mui/Avatar.tsx
new file mode 100644
index 00000000..963984ab
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Avatar.tsx
@@ -0,0 +1,5 @@
+import { h, Fragment, VNode, ComponentChildren } from "preact";
+
+export function Avatar({}: { children: ComponentChildren }): VNode {
+  return <Fragment />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx 
b/packages/taler-wallet-webextension/src/mui/Button.tsx
new file mode 100644
index 00000000..b197ca26
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Button.tsx
@@ -0,0 +1,215 @@
+import { ComponentChildren, h, VNode } from "preact";
+import { css } from "@linaria/core";
+import { theme, ripple } from "./style";
+import { alpha } from "./colors/manipulation";
+
+interface Props {
+  children?: ComponentChildren;
+  disabled?: boolean;
+  disableElevation?: boolean;
+  disableFocusRipple?: boolean;
+  endIcon?: VNode;
+  fullWidth?: boolean;
+  href?: string;
+  size?: "small" | "medium" | "large";
+  startIcon?: VNode;
+  variant?: "contained" | "outlined" | "text";
+  color?: "primary" | "secondary" | "success" | "error" | "info" | "warning";
+}
+
+const baseStyle = css`
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  box-sizing: border-box;
+  background-color: transparent;
+  outline: 0;
+  border: 0;
+  margin: 0;
+  border-radius: 0;
+  padding: 0;
+  cursor: pointer;
+  user-select: none;
+  vertical-align: middle;
+  text-decoration: none;
+  color: inherit;
+`;
+
+const button = css`
+  min-width: 64px;
+  &:hover {
+    text-decoration: none;
+    background-color: var(--text-primary-alpha-opacity);
+    @media (hover: none) {
+      background-color: transparent;
+    }
+  }
+  &:disabled {
+    color: ${theme.palette.action.disabled};
+  }
+`;
+
+const colorVariant = {
+  outlined: css`
+    color: var(--color-main);
+    border: 1px solid var(--color-main-alpha-half);
+    &:hover {
+      border: 1px solid var(--color-main);
+      background-color: var(--color-main-alpha-opacity);
+    }
+    &:disabled {
+      border: 1px solid ${theme.palette.action.disabledBackground};
+    }
+  `,
+  contained: css`
+    color: var(--color-contrastText);
+    background-color: var(--color-main);
+    box-shadow: ${theme.shadows[2]};
+    &:hover {
+      background-color: var(--color-dark);
+    }
+    &:active {
+      box-shadow: ${theme.shadows[8]};
+    }
+    &:focus-visible {
+      box-shadow: ${theme.shadows[6]};
+    }
+    &:disabled {
+      color: ${theme.palette.action.disabled};
+      box-shadow: ${theme.shadows[0]};
+      background-color: ${theme.palette.action.disabledBackground};
+    }
+  `,
+  text: css`
+    color: var(--color-main);
+    &:hover {
+      background-color: var(--color-main-alpha-opacity);
+    }
+  `,
+};
+
+const sizeVariant = {
+  outlined: {
+    small: css`
+      padding: 3px 9px;
+      font-size: ${theme.pxToRem(13)};
+    `,
+    medium: css`
+      padding: 5px 15px;
+    `,
+    large: css`
+      padding: 7px 21px;
+      font-size: ${theme.pxToRem(15)};
+    `,
+  },
+  contained: {
+    small: css`
+      padding: 4px 10px;
+      font-size: ${theme.pxToRem(13)};
+    `,
+    medium: css`
+      padding: 6px 16px;
+    `,
+    large: css`
+      padding: 8px 22px;
+      font-size: ${theme.pxToRem(15)};
+    `,
+  },
+  text: {
+    small: css`
+      padding: 4px 5px;
+      font-size: ${theme.pxToRem(13)};
+    `,
+    medium: css`
+      padding: 6px 8px;
+    `,
+    large: css`
+      padding: 8px 11px;
+      font-size: ${theme.pxToRem(15)};
+    `,
+  },
+};
+
+export function Button({
+  children,
+  disabled,
+  startIcon: sip,
+  endIcon: eip,
+  variant = "text",
+  size = "medium",
+  color = "primary",
+}: Props): VNode {
+  const style = css`
+    user-select: none;
+    width: 1em;
+    height: 1em;
+    display: inline-block;
+    fill: currentColor;
+    flex-shrink: 0;
+    transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
+
+    & > svg {
+      font-size: 20;
+    }
+  `;
+
+  const startIcon = sip && (
+    <span
+      class={[
+        css`
+          margin-right: 8px;
+          margin-left: -4px;
+        `,
+        style,
+      ].join(" ")}
+    >
+      {sip}
+    </span>
+  );
+  const endIcon = eip && (
+    <span
+      class={[
+        css`
+          margin-right: -4px;
+          margin-left: 8px;
+        `,
+        style,
+      ].join(" ")}
+    >
+      {eip}
+    </span>
+  );
+  return (
+    <button
+      disabled={disabled}
+      class={[
+        theme.typography.button,
+        theme.shape.borderRadius,
+        ripple,
+        baseStyle,
+        button,
+        colorVariant[variant],
+        sizeVariant[variant][size],
+      ].join(" ")}
+      style={{
+        "--color-main": theme.palette[color].main,
+        "--color-main-alpha-half": alpha(theme.palette[color].main, 0.5),
+        "--color-contrastText": theme.palette[color].contrastText,
+        "--color-dark": theme.palette[color].dark,
+        "--color-main-alpha-opacity": alpha(
+          theme.palette[color].main,
+          theme.palette.action.hoverOpacity,
+        ),
+        "--text-primary-alpha-opacity": alpha(
+          theme.palette.text.primary,
+          theme.palette.action.hoverOpacity,
+        ),
+      }}
+    >
+      {startIcon}
+      {children}
+      {endIcon}
+    </button>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Divider.tsx 
b/packages/taler-wallet-webextension/src/mui/Divider.tsx
new file mode 100644
index 00000000..27ab392f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Divider.tsx
@@ -0,0 +1,5 @@
+import { h, Fragment, VNode } from "preact";
+
+export function Divider(): VNode {
+  return <Fragment />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Grid.tsx 
b/packages/taler-wallet-webextension/src/mui/Grid.tsx
new file mode 100644
index 00000000..3974e3c2
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Grid.tsx
@@ -0,0 +1,13 @@
+import { h, Fragment, VNode, ComponentChildren } from "preact";
+
+export function Grid({}: {
+  container?: boolean;
+  wrap?: string;
+  item?: boolean;
+  spacing?: number;
+  alignItems?: string;
+  justify?: string;
+  children: ComponentChildren;
+}): VNode {
+  return <Fragment />;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx
new file mode 100644
index 00000000..f263526f
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Paper.stories.tsx
@@ -0,0 +1,149 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { Paper } from "./Paper";
+import { createExample } from "../test-utils";
+import { h } from "preact";
+
+export default {
+  title: "mui/paper",
+  component: Paper,
+};
+
+export const BasicExample = () => (
+  <div
+    style={{
+      display: "flex",
+      wrap: "nowrap",
+      backgroundColor: "lightgray",
+      width: "100%",
+      padding: 10,
+      justifyContent: "space-between",
+    }}
+  >
+    <Paper elevation={0}>
+      <div style={{ height: 128, width: 128 }} />
+    </Paper>
+    <Paper>
+      <div style={{ height: 128, width: 128 }} />
+    </Paper>
+    <Paper elevation={3}>
+      <div style={{ height: 128, width: 128 }} />
+    </Paper>
+    <Paper elevation={8}>
+      <div style={{ height: 128, width: 128 }} />
+    </Paper>
+  </div>
+);
+
+export const Outlined = () => (
+  <div
+    style={{
+      display: "flex",
+      wrap: "nowrap",
+      backgroundColor: "lightgray",
+      width: "100%",
+      padding: 10,
+      justifyContent: "space-around",
+    }}
+  >
+    <Paper variant="outlined">
+      <div
+        style={{
+          textAlign: "center",
+          height: 128,
+          width: 128,
+          lineHeight: "128px",
+        }}
+      >
+        round
+      </div>
+    </Paper>
+    <Paper variant="outlined" square>
+      <div
+        style={{
+          textAlign: "center",
+          height: 128,
+          width: 128,
+          lineHeight: "128px",
+        }}
+      >
+        square
+      </div>
+    </Paper>
+  </div>
+);
+
+export const Elevation = () => (
+  <div
+    style={{
+      display: "flex",
+      flexDirection: "column",
+      backgroundColor: "lightgray",
+      width: "100%",
+      padding: 50,
+      justifyContent: "space-around",
+    }}
+  >
+    {[0, 1, 2, 3, 4, 6, 8, 12, 16, 24].map((elevation) => (
+      <div style={{ marginTop: 50 }} key={elevation}>
+        <Paper elevation={elevation}>
+          <div
+            style={{
+              textAlign: "center",
+              height: 60,
+              lineHeight: "60px",
+            }}
+          >{`elevation=${elevation}`}</div>
+        </Paper>
+      </div>
+    ))}
+  </div>
+);
+
+export const ElevationDark = () => (
+  <div
+    class="theme-dark"
+    style={{
+      display: "flex",
+      flexDirection: "column",
+      backgroundColor: "lightgray",
+      width: "100%",
+      padding: 50,
+      justifyContent: "space-around",
+    }}
+  >
+    to be implemented
+    {/* {[0, 1, 2, 3, 4, 6, 8, 12, 16, 24].map((elevation) => (
+      <div style={{ marginTop: 50 }} key={elevation}>
+        <Paper elevation={elevation}>
+          <div
+            style={{
+              textAlign: "center",
+              height: 60,
+              lineHeight: "60px",
+            }}
+          >{`elevation=${elevation}`}</div>
+        </Paper>
+      </div>
+    ))} */}
+  </div>
+);
diff --git a/packages/taler-wallet-webextension/src/mui/Paper.tsx 
b/packages/taler-wallet-webextension/src/mui/Paper.tsx
new file mode 100644
index 00000000..52524380
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Paper.tsx
@@ -0,0 +1,63 @@
+import { css } from "@linaria/core";
+import { h, Fragment, VNode, ComponentChildren } from "preact";
+import { alpha } from "./colors/manipulation";
+import { theme } from "./style";
+
+const borderVariant = {
+  outlined: css`
+    border: 1px solid ${theme.palette.divider};
+  `,
+  elevation: css`
+    box-shadow: var(--theme-shadow-elevation);
+  `,
+};
+const baseStyle = css`
+  background-color: ${theme.palette.background.paper};
+  color: ${theme.palette.text.primary};
+
+  .theme-dark & {
+    background-image: var(--gradient-white-elevation);
+  }
+`;
+
+export function Paper({
+  elevation = 1,
+  square,
+  variant = "elevation",
+  children,
+}: {
+  elevation?: number;
+  square?: boolean;
+  variant?: "elevation" | "outlined";
+  children?: ComponentChildren;
+}): VNode {
+  return (
+    <div
+      class={[
+        baseStyle,
+        !square && theme.shape.borderRadius,
+        borderVariant[variant],
+      ].join(" ")}
+      style={{
+        "--theme-shadow-elevation": theme.shadows[elevation],
+        "--gradient-white-elevation": `linear-gradient(${alpha(
+          "#fff",
+          getOverlayAlpha(elevation),
+        )}, ${alpha("#fff", getOverlayAlpha(elevation))})`,
+      }}
+    >
+      {children}
+    </div>
+  );
+}
+
+// Inspired by 
https://github.com/material-components/material-components-ios/blob/bca36107405594d5b7b16265a5b0ed698f85a5ee/components/Elevation/src/UIColor%2BMaterialElevation.m#L61
+const getOverlayAlpha = (elevation: number): number => {
+  let alphaValue;
+  if (elevation < 1) {
+    alphaValue = 5.11916 * elevation ** 2;
+  } else {
+    alphaValue = 4.5 * Math.log(elevation + 1) + 2;
+  }
+  return Number((alphaValue / 100).toFixed(2));
+};
diff --git a/packages/taler-wallet-webextension/src/mui/Typography.tsx 
b/packages/taler-wallet-webextension/src/mui/Typography.tsx
new file mode 100644
index 00000000..4fc61446
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/Typography.tsx
@@ -0,0 +1,9 @@
+import { h, Fragment, VNode, ComponentChildren } from "preact";
+
+interface Props {
+  children: ComponentChildren;
+}
+
+export function Typography({ children }: Props): VNode {
+  return <p>{children}</p>;
+}
diff --git a/packages/taler-wallet-webextension/src/mui/colors/constants.ts 
b/packages/taler-wallet-webextension/src/mui/colors/constants.ts
new file mode 100644
index 00000000..a6e58caa
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/constants.ts
@@ -0,0 +1,348 @@
+export const amber = {
+  50: '#fff8e1',
+  100: '#ffecb3',
+  200: '#ffe082',
+  300: '#ffd54f',
+  400: '#ffca28',
+  500: '#ffc107',
+  600: '#ffb300',
+  700: '#ffa000',
+  800: '#ff8f00',
+  900: '#ff6f00',
+  A100: '#ffe57f',
+  A200: '#ffd740',
+  A400: '#ffc400',
+  A700: '#ffab00',
+};
+
+
+export const blueGrey = {
+  50: '#eceff1',
+  100: '#cfd8dc',
+  200: '#b0bec5',
+  300: '#90a4ae',
+  400: '#78909c',
+  500: '#607d8b',
+  600: '#546e7a',
+  700: '#455a64',
+  800: '#37474f',
+  900: '#263238',
+  A100: '#cfd8dc',
+  A200: '#b0bec5',
+  A400: '#78909c',
+  A700: '#455a64',
+};
+
+
+export const blue = {
+  50: '#e3f2fd',
+  100: '#bbdefb',
+  200: '#90caf9',
+  300: '#64b5f6',
+  400: '#42a5f5',
+  500: '#2196f3',
+  600: '#1e88e5',
+  700: '#1976d2',
+  800: '#1565c0',
+  900: '#0d47a1',
+  A100: '#82b1ff',
+  A200: '#448aff',
+  A400: '#2979ff',
+  A700: '#2962ff',
+};
+
+
+export const brown = {
+  50: '#efebe9',
+  100: '#d7ccc8',
+  200: '#bcaaa4',
+  300: '#a1887f',
+  400: '#8d6e63',
+  500: '#795548',
+  600: '#6d4c41',
+  700: '#5d4037',
+  800: '#4e342e',
+  900: '#3e2723',
+  A100: '#d7ccc8',
+  A200: '#bcaaa4',
+  A400: '#8d6e63',
+  A700: '#5d4037',
+};
+
+
+export const common = {
+  black: '#000',
+  white: '#fff',
+};
+
+
+export const cyan = {
+  50: '#e0f7fa',
+  100: '#b2ebf2',
+  200: '#80deea',
+  300: '#4dd0e1',
+  400: '#26c6da',
+  500: '#00bcd4',
+  600: '#00acc1',
+  700: '#0097a7',
+  800: '#00838f',
+  900: '#006064',
+  A100: '#84ffff',
+  A200: '#18ffff',
+  A400: '#00e5ff',
+  A700: '#00b8d4',
+};
+
+
+export const deepOrange = {
+  50: '#fbe9e7',
+  100: '#ffccbc',
+  200: '#ffab91',
+  300: '#ff8a65',
+  400: '#ff7043',
+  500: '#ff5722',
+  600: '#f4511e',
+  700: '#e64a19',
+  800: '#d84315',
+  900: '#bf360c',
+  A100: '#ff9e80',
+  A200: '#ff6e40',
+  A400: '#ff3d00',
+  A700: '#dd2c00',
+};
+
+
+export const deepPurple = {
+  50: '#ede7f6',
+  100: '#d1c4e9',
+  200: '#b39ddb',
+  300: '#9575cd',
+  400: '#7e57c2',
+  500: '#673ab7',
+  600: '#5e35b1',
+  700: '#512da8',
+  800: '#4527a0',
+  900: '#311b92',
+  A100: '#b388ff',
+  A200: '#7c4dff',
+  A400: '#651fff',
+  A700: '#6200ea',
+};
+
+
+export const green = {
+  50: '#e8f5e9',
+  100: '#c8e6c9',
+  200: '#a5d6a7',
+  300: '#81c784',
+  400: '#66bb6a',
+  500: '#4caf50',
+  600: '#43a047',
+  700: '#388e3c',
+  800: '#2e7d32',
+  900: '#1b5e20',
+  A100: '#b9f6ca',
+  A200: '#69f0ae',
+  A400: '#00e676',
+  A700: '#00c853',
+};
+
+
+export const grey = {
+  50: '#fafafa',
+  100: '#f5f5f5',
+  200: '#eeeeee',
+  300: '#e0e0e0',
+  400: '#bdbdbd',
+  500: '#9e9e9e',
+  600: '#757575',
+  700: '#616161',
+  800: '#424242',
+  900: '#212121',
+  A100: '#f5f5f5',
+  A200: '#eeeeee',
+  A400: '#bdbdbd',
+  A700: '#616161',
+};
+
+
+export const indigo = {
+  50: '#e8eaf6',
+  100: '#c5cae9',
+  200: '#9fa8da',
+  300: '#7986cb',
+  400: '#5c6bc0',
+  500: '#3f51b5',
+  600: '#3949ab',
+  700: '#303f9f',
+  800: '#283593',
+  900: '#1a237e',
+  A100: '#8c9eff',
+  A200: '#536dfe',
+  A400: '#3d5afe',
+  A700: '#304ffe',
+};
+
+
+export const lightBlue = {
+  50: '#e1f5fe',
+  100: '#b3e5fc',
+  200: '#81d4fa',
+  300: '#4fc3f7',
+  400: '#29b6f6',
+  500: '#03a9f4',
+  600: '#039be5',
+  700: '#0288d1',
+  800: '#0277bd',
+  900: '#01579b',
+  A100: '#80d8ff',
+  A200: '#40c4ff',
+  A400: '#00b0ff',
+  A700: '#0091ea',
+};
+
+
+export const lightGreen = {
+  50: '#f1f8e9',
+  100: '#dcedc8',
+  200: '#c5e1a5',
+  300: '#aed581',
+  400: '#9ccc65',
+  500: '#8bc34a',
+  600: '#7cb342',
+  700: '#689f38',
+  800: '#558b2f',
+  900: '#33691e',
+  A100: '#ccff90',
+  A200: '#b2ff59',
+  A400: '#76ff03',
+  A700: '#64dd17',
+};
+
+
+export const lime = {
+  50: '#f9fbe7',
+  100: '#f0f4c3',
+  200: '#e6ee9c',
+  300: '#dce775',
+  400: '#d4e157',
+  500: '#cddc39',
+  600: '#c0ca33',
+  700: '#afb42b',
+  800: '#9e9d24',
+  900: '#827717',
+  A100: '#f4ff81',
+  A200: '#eeff41',
+  A400: '#c6ff00',
+  A700: '#aeea00',
+};
+
+
+export const orange = {
+  50: '#fff3e0',
+  100: '#ffe0b2',
+  200: '#ffcc80',
+  300: '#ffb74d',
+  400: '#ffa726',
+  500: '#ff9800',
+  600: '#fb8c00',
+  700: '#f57c00',
+  800: '#ef6c00',
+  900: '#e65100',
+  A100: '#ffd180',
+  A200: '#ffab40',
+  A400: '#ff9100',
+  A700: '#ff6d00',
+};
+
+
+export const pink = {
+  50: '#fce4ec',
+  100: '#f8bbd0',
+  200: '#f48fb1',
+  300: '#f06292',
+  400: '#ec407a',
+  500: '#e91e63',
+  600: '#d81b60',
+  700: '#c2185b',
+  800: '#ad1457',
+  900: '#880e4f',
+  A100: '#ff80ab',
+  A200: '#ff4081',
+  A400: '#f50057',
+  A700: '#c51162',
+};
+
+
+export const purple = {
+  50: '#f3e5f5',
+  100: '#e1bee7',
+  200: '#ce93d8',
+  300: '#ba68c8',
+  400: '#ab47bc',
+  500: '#9c27b0',
+  600: '#8e24aa',
+  700: '#7b1fa2',
+  800: '#6a1b9a',
+  900: '#4a148c',
+  A100: '#ea80fc',
+  A200: '#e040fb',
+  A400: '#d500f9',
+  A700: '#aa00ff',
+};
+
+
+export const red = {
+  50: '#ffebee',
+  100: '#ffcdd2',
+  200: '#ef9a9a',
+  300: '#e57373',
+  400: '#ef5350',
+  500: '#f44336',
+  600: '#e53935',
+  700: '#d32f2f',
+  800: '#c62828',
+  900: '#b71c1c',
+  A100: '#ff8a80',
+  A200: '#ff5252',
+  A400: '#ff1744',
+  A700: '#d50000',
+};
+
+
+export const teal = {
+  50: '#e0f2f1',
+  100: '#b2dfdb',
+  200: '#80cbc4',
+  300: '#4db6ac',
+  400: '#26a69a',
+  500: '#009688',
+  600: '#00897b',
+  700: '#00796b',
+  800: '#00695c',
+  900: '#004d40',
+  A100: '#a7ffeb',
+  A200: '#64ffda',
+  A400: '#1de9b6',
+  A700: '#00bfa5',
+};
+
+
+export const yellow = {
+  50: '#fffde7',
+  100: '#fff9c4',
+  200: '#fff59d',
+  300: '#fff176',
+  400: '#ffee58',
+  500: '#ffeb3b',
+  600: '#fdd835',
+  700: '#fbc02d',
+  800: '#f9a825',
+  900: '#f57f17',
+  A100: '#ffff8d',
+  A200: '#ffff00',
+  A400: '#ffea00',
+  A700: '#ffd600',
+};
+
+
diff --git 
a/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts 
b/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts
new file mode 100644
index 00000000..77b3ec88
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts
@@ -0,0 +1,305 @@
+import { expect } from 'chai';
+import {
+  recomposeColor,
+  hexToRgb,
+  rgbToHex,
+  hslToRgb,
+  darken,
+  decomposeColor,
+  emphasize,
+  alpha,
+  getContrastRatio,
+  getLuminance,
+  lighten,
+} from './manipulation';
+
+describe('utils/colorManipulator', () => {
+  describe('recomposeColor', () => {
+    it('converts a decomposed rgb color object to a string` ', () => {
+      expect(
+        recomposeColor({
+          type: 'rgb',
+          values: [255, 255, 255],
+        }),
+      ).to.equal('rgb(255, 255, 255)');
+    });
+
+    it('converts a decomposed rgba color object to a string` ', () => {
+      expect(
+        recomposeColor({
+          type: 'rgba',
+          values: [255, 255, 255, 0.5],
+        }),
+      ).to.equal('rgba(255, 255, 255, 0.5)');
+    });
+
+    it('converts a decomposed hsl color object to a string` ', () => {
+      expect(
+        recomposeColor({
+          type: 'hsl',
+          values: [100, 50, 25],
+        }),
+      ).to.equal('hsl(100, 50%, 25%)');
+    });
+
+    it('converts a decomposed hsla color object to a string` ', () => {
+      expect(
+        recomposeColor({
+          type: 'hsla',
+          values: [100, 50, 25, 0.5],
+        }),
+      ).to.equal('hsla(100, 50%, 25%, 0.5)');
+    });
+  });
+
+  describe('hexToRgb', () => {
+    it('converts a short hex color to an rgb color` ', () => {
+      expect(hexToRgb('#9f3')).to.equal('rgb(153, 255, 51)');
+    });
+
+    it('converts a long hex color to an rgb color` ', () => {
+      expect(hexToRgb('#a94fd3')).to.equal('rgb(169, 79, 211)');
+    });
+
+    it('converts a long alpha hex color to an argb color` ', () => {
+      expect(hexToRgb('#111111f8')).to.equal('rgba(17, 17, 17, 0.973)');
+    });
+  });
+
+  describe('rgbToHex', () => {
+    it('converts an rgb color to a hex color` ', () => {
+      expect(rgbToHex('rgb(169, 79, 211)')).to.equal('#a94fd3');
+    });
+
+    it('converts an rgba color to a hex color` ', () => {
+      expect(rgbToHex('rgba(169, 79, 211, 1)')).to.equal('#a94fd3ff');
+    });
+
+    it('idempotent', () => {
+      expect(rgbToHex('#A94FD3')).to.equal('#A94FD3');
+    });
+  });
+
+  describe('hslToRgb', () => {
+    it('converts an hsl color to an rgb color` ', () => {
+      expect(hslToRgb('hsl(281, 60%, 57%)')).to.equal('rgb(169, 80, 211)');
+    });
+
+    it('converts an hsla color to an rgba color` ', () => {
+      expect(hslToRgb('hsla(281, 60%, 57%, 0.5)')).to.equal('rgba(169, 80, 
211, 0.5)');
+    });
+
+    it('allow to convert values only', () => {
+      expect(hslToRgb('hsl(281, 60%, 57%)')).to.equal('rgb(169, 80, 211)');
+    });
+  });
+
+  describe('decomposeColor', () => {
+    it('converts an rgb color string to an object with `type` and `value` 
keys', () => {
+      const { type, values } = decomposeColor('rgb(255, 255, 255)');
+      expect(type).to.equal('rgb');
+      expect(values).to.deep.equal([255, 255, 255]);
+    });
+
+    it('converts an rgba color string to an object with `type` and `value` 
keys', () => {
+      const { type, values } = decomposeColor('rgba(255, 255, 255, 0.5)');
+      expect(type).to.equal('rgba');
+      expect(values).to.deep.equal([255, 255, 255, 0.5]);
+    });
+
+    it('converts an hsl color string to an object with `type` and `value` 
keys', () => {
+      const { type, values } = decomposeColor('hsl(100, 50%, 25%)');
+      expect(type).to.equal('hsl');
+      expect(values).to.deep.equal([100, 50, 25]);
+    });
+
+    it('converts an hsla color string to an object with `type` and `value` 
keys', () => {
+      const { type, values } = decomposeColor('hsla(100, 50%, 25%, 0.5)');
+      expect(type).to.equal('hsla');
+      expect(values).to.deep.equal([100, 50, 25, 0.5]);
+    });
+
+    it('converts rgba hex', () => {
+      const decomposed = decomposeColor('#111111f8');
+      expect(decomposed).to.deep.equal({
+        type: 'rgba',
+        colorSpace: undefined,
+        values: [17, 17, 17, 0.973],
+      });
+    });
+  });
+
+  describe('getContrastRatio', () => {
+    it('returns a ratio for black : white', () => {
+      expect(getContrastRatio('#000', '#FFF')).to.equal(21);
+    });
+
+    it('returns a ratio for black : black', () => {
+      expect(getContrastRatio('#000', '#000')).to.equal(1);
+    });
+
+    it('returns a ratio for white : white', () => {
+      expect(getContrastRatio('#FFF', '#FFF')).to.equal(1);
+    });
+
+    it('returns a ratio for dark-grey : light-grey', () => {
+      expect(getContrastRatio('#707070', '#E5E5E5')).to.be.approximately(3.93, 
0.01);
+    });
+
+    it('returns a ratio for black : light-grey', () => {
+      expect(getContrastRatio('#000', '#888')).to.be.approximately(5.92, 0.01);
+    });
+  });
+
+  describe('getLuminance', () => {
+
+    it('returns a valid luminance for rgb white ', () => {
+      expect(getLuminance('rgba(255, 255, 255)')).to.equal(1);
+      expect(getLuminance('rgb(255, 255, 255)')).to.equal(1);
+    });
+
+    it('returns a valid luminance for rgb mid-grey', () => {
+      expect(getLuminance('rgba(127, 127, 127)')).to.equal(0.212);
+      expect(getLuminance('rgb(127, 127, 127)')).to.equal(0.212);
+    });
+
+    it('returns a valid luminance for an rgb color', () => {
+      expect(getLuminance('rgb(255, 127, 0)')).to.equal(0.364);
+    });
+
+    it('returns a valid luminance from an hsl color', () => {
+      expect(getLuminance('hsl(100, 100%, 50%)')).to.equal(0.735);
+    });
+
+    it('returns an equal luminance for the same color in different formats', 
() => {
+      const hsl = 'hsl(100, 100%, 50%)';
+      const rgb = 'rgb(85, 255, 0)';
+      expect(getLuminance(hsl)).to.equal(getLuminance(rgb));
+    });
+
+  });
+
+  describe('emphasize', () => {
+    it('lightens a dark rgb color with the coefficient provided', () => {
+      expect(emphasize('rgb(1, 2, 3)', 0.4)).to.equal(lighten('rgb(1, 2, 3)', 
0.4));
+    });
+
+    it('darkens a light rgb color with the coefficient provided', () => {
+      expect(emphasize('rgb(250, 240, 230)', 0.3)).to.equal(darken('rgb(250, 
240, 230)', 0.3));
+    });
+
+    it('lightens a dark rgb color with the coefficient 0.15 by default', () => 
{
+      expect(emphasize('rgb(1, 2, 3)')).to.equal(lighten('rgb(1, 2, 3)', 
0.15));
+    });
+
+    it('darkens a light rgb color with the coefficient 0.15 by default', () => 
{
+      expect(emphasize('rgb(250, 240, 230)')).to.equal(darken('rgb(250, 240, 
230)', 0.15));
+    });
+
+  });
+
+  describe('alpha', () => {
+    it('converts an rgb color to an rgba color with the value provided', () => 
{
+      expect(alpha('rgb(1, 2, 3)', 0.4)).to.equal('rgba(1, 2, 3, 0.4)');
+    });
+
+    it('updates an rgba color with the alpha value provided', () => {
+      expect(alpha('rgba(255, 0, 0, 0.2)', 0.5)).to.equal('rgba(255, 0, 0, 
0.5)');
+    });
+
+    it('converts an hsl color to an hsla color with the value provided', () => 
{
+      expect(alpha('hsl(0, 100%, 50%)', 0.1)).to.equal('hsla(0, 100%, 50%, 
0.1)');
+    });
+
+    it('updates an hsla color with the alpha value provided', () => {
+      expect(alpha('hsla(0, 100%, 50%, 0.2)', 0.5)).to.equal('hsla(0, 100%, 
50%, 0.5)');
+    });
+
+  });
+
+  describe('darken', () => {
+    it("doesn't modify rgb black", () => {
+      expect(darken('rgb(0, 0, 0)', 0.1)).to.equal('rgb(0, 0, 0)');
+    });
+
+    it('darkens rgb white to black when coefficient is 1', () => {
+      expect(darken('rgb(255, 255, 255)', 1)).to.equal('rgb(0, 0, 0)');
+    });
+
+    it('retains the alpha value in an rgba color', () => {
+      expect(darken('rgba(0, 0, 0, 0.5)', 0.1)).to.equal('rgba(0, 0, 0, 0.5)');
+    });
+
+    it('darkens rgb white by 10% when coefficient is 0.1', () => {
+      expect(darken('rgb(255, 255, 255)', 0.1)).to.equal('rgb(229, 229, 229)');
+    });
+
+    it('darkens rgb red by 50% when coefficient is 0.5', () => {
+      expect(darken('rgb(255, 0, 0)', 0.5)).to.equal('rgb(127, 0, 0)');
+    });
+
+    it('darkens rgb grey by 50% when coefficient is 0.5', () => {
+      expect(darken('rgb(127, 127, 127)', 0.5)).to.equal('rgb(63, 63, 63)');
+    });
+
+    it("doesn't modify rgb colors when coefficient is 0", () => {
+      expect(darken('rgb(255, 255, 255)', 0)).to.equal('rgb(255, 255, 255)');
+    });
+
+    it('darkens hsl red by 50% when coefficient is 0.5', () => {
+      expect(darken('hsl(0, 100%, 50%)', 0.5)).to.equal('hsl(0, 100%, 25%)');
+    });
+
+    it("doesn't modify hsl colors when coefficient is 0", () => {
+      expect(darken('hsl(0, 100%, 50%)', 0)).to.equal('hsl(0, 100%, 50%)');
+    });
+
+    it("doesn't modify hsl colors when l is 0%", () => {
+      expect(darken('hsl(0, 50%, 0%)', 0.5)).to.equal('hsl(0, 50%, 0%)');
+    });
+
+  });
+
+  describe('lighten', () => {
+    it("doesn't modify rgb white", () => {
+      expect(lighten('rgb(255, 255, 255)', 0.1)).to.equal('rgb(255, 255, 
255)');
+    });
+
+    it('lightens rgb black to white when coefficient is 1', () => {
+      expect(lighten('rgb(0, 0, 0)', 1)).to.equal('rgb(255, 255, 255)');
+    });
+
+    it('retains the alpha value in an rgba color', () => {
+      expect(lighten('rgba(255, 255, 255, 0.5)', 0.1)).to.equal('rgba(255, 
255, 255, 0.5)');
+    });
+
+    it('lightens rgb black by 10% when coefficient is 0.1', () => {
+      expect(lighten('rgb(0, 0, 0)', 0.1)).to.equal('rgb(25, 25, 25)');
+    });
+
+    it('lightens rgb red by 50% when coefficient is 0.5', () => {
+      expect(lighten('rgb(255, 0, 0)', 0.5)).to.equal('rgb(255, 127, 127)');
+    });
+
+    it('lightens rgb grey by 50% when coefficient is 0.5', () => {
+      expect(lighten('rgb(127, 127, 127)', 0.5)).to.equal('rgb(191, 191, 
191)');
+    });
+
+    it("doesn't modify rgb colors when coefficient is 0", () => {
+      expect(lighten('rgb(127, 127, 127)', 0)).to.equal('rgb(127, 127, 127)');
+    });
+
+    it('lightens hsl red by 50% when coefficient is 0.5', () => {
+      expect(lighten('hsl(0, 100%, 50%)', 0.5)).to.equal('hsl(0, 100%, 75%)');
+    });
+
+    it("doesn't modify hsl colors when coefficient is 0", () => {
+      expect(lighten('hsl(0, 100%, 50%)', 0)).to.equal('hsl(0, 100%, 50%)');
+    });
+
+    it("doesn't modify hsl colors when `l` is 100%", () => {
+      expect(lighten('hsl(0, 50%, 100%)', 0.5)).to.equal('hsl(0, 50%, 100%)');
+    });
+
+  });
+});
diff --git a/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts 
b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
new file mode 100644
index 00000000..633c80c9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/colors/manipulation.ts
@@ -0,0 +1,273 @@
+
+export type ColorFormat = ColorFormatWithAlpha | ColorFormatWithoutAlpha
+export type ColorFormatWithAlpha = 'rgb' | 'hsl';
+export type ColorFormatWithoutAlpha = 'rgba' | 'hsla';
+export type ColorObject = ColorObjectWithAlpha | ColorObjectWithoutAlpha
+export interface ColorObjectWithAlpha {
+  type: ColorFormatWithAlpha;
+  values: [number, number, number];
+  colorSpace?: 'srgb' | 'display-p3' | 'a98-rgb' | 'prophoto-rgb' | 'rec-2020';
+}
+export interface ColorObjectWithoutAlpha {
+  type: ColorFormatWithoutAlpha;
+  values: [number, number, number, number];
+  colorSpace?: 'srgb' | 'display-p3' | 'a98-rgb' | 'prophoto-rgb' | 'rec-2020';
+}
+
+
+/**
+ * Returns a number whose value is limited to the given range.
+ * @param {number} value The value to be clamped
+ * @param {number} min The lower boundary of the output range
+ * @param {number} max The upper boundary of the output range
+ * @returns {number} A number in the range [min, max]
+ */
+function clamp(value: number, min: number = 0, max: number = 1): number {
+  // if (process.env.NODE_ENV !== 'production') {
+  //   if (value < min || value > max) {
+  //     console.error(`MUI: The value provided ${value} is out of range 
[${min}, ${max}].`);
+  //   }
+  // }
+
+  return Math.min(Math.max(min, value), max);
+}
+
+/**
+ * Converts a color from CSS hex format to CSS rgb format.
+ * @param {string} color - Hex color, i.e. #nnn or #nnnnnn
+ * @returns {string} A CSS rgb color string
+ */
+export function hexToRgb(color: string): string {
+  color = color.substr(1);
+
+  const re = new RegExp(`.{1,${color.length >= 6 ? 2 : 1}}`, 'g');
+  let colors = color.match(re);
+
+  if (colors && colors[0].length === 1) {
+    colors = colors.map((n) => n + n);
+  }
+
+  return colors
+    ? `rgb${colors.length === 4 ? 'a' : ''}(${colors
+      .map((n, index) => {
+        return index < 3 ? parseInt(n, 16) : Math.round((parseInt(n, 16) / 
255) * 1000) / 1000;
+      })
+      .join(', ')})`
+    : '';
+}
+
+function intToHex(int: number): string {
+  const hex = int.toString(16);
+  return hex.length === 1 ? `0${hex}` : hex;
+}
+
+/**
+ * Returns an object with the type and values of a color.
+ *
+ * Note: Does not support rgb % values.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla()
+ * @returns {object} - A MUI color object: {type: string, values: number[]}
+ */
+export function decomposeColor(color: string): ColorObject {
+  const colorSpace = undefined;
+  if (color.charAt(0) === '#') {
+    return decomposeColor(hexToRgb(color));
+  }
+
+  const marker = color.indexOf('(');
+  const type = color.substring(0, marker);
+  if (type != 'rgba' && type != 'hsla' && type != 'rgb' && type != 'hsl') {
+  }
+
+  const values = color.substring(marker + 1, color.length - 1).split(',')
+  if (type == 'rgb' || type == 'hsl') {
+    return { type, colorSpace, values: [parseFloat(values[0]), 
parseFloat(values[1]), parseFloat(values[2])] }
+  }
+  if (type == 'rgba' || type == 'hsla') {
+    return { type, colorSpace, values: [parseFloat(values[0]), 
parseFloat(values[1]), parseFloat(values[2]), parseFloat(values[3])] }
+  }
+  throw new Error(`Unsupported '${color}' color. The following formats are 
supported: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla()`)
+}
+
+/**
+ * Converts a color object with type and values to a string.
+ * @param {object} color - Decomposed color
+ * @param {string} color.type - One of: 'rgb', 'rgba', 'hsl', 'hsla'
+ * @param {array} color.values - [n,n,n] or [n,n,n,n]
+ * @returns {string} A CSS color string
+ */
+export function recomposeColor(color: ColorObject): string {
+  const { type, values: valuesNum } = color;
+
+  const valuesStr: string[] = [];
+  if (type.indexOf('rgb') !== -1) {
+    // Only convert the first 3 values to int (i.e. not alpha)
+    valuesNum.map((n, i) => (i < 3 ? parseInt(String(n), 10) : n)).forEach((n, 
i) => valuesStr[i] = String(n));
+  } else if (type.indexOf('hsl') !== -1) {
+    valuesStr[0] = String(valuesNum[0])
+    valuesStr[1] = `${valuesNum[1]}%`;
+    valuesStr[2] = `${valuesNum[2]}%`;
+    if (type === 'hsla') {
+      valuesStr[3] = String(valuesNum[3])
+    }
+  }
+
+  return `${type}(${valuesStr.join(', ')})`;
+}
+
+/**
+ * Converts a color from CSS rgb format to CSS hex format.
+ * @param {string} color - RGB color, i.e. rgb(n, n, n)
+ * @returns {string} A CSS rgb color string, i.e. #nnnnnn
+ */
+export function rgbToHex(color: string): string {
+  // Idempotent
+  if (color.indexOf('#') === 0) {
+    return color;
+  }
+
+  const { values } = decomposeColor(color);
+  return `#${values.map((n, i) => intToHex(i === 3 ? Math.round(255 * n) : 
n)).join('')}`;
+}
+
+/**
+ * Converts a color from hsl format to rgb format.
+ * @param {string} color - HSL color values
+ * @returns {string} rgb color values
+ */
+export function hslToRgb(color: string): string {
+  const colorObj = decomposeColor(color);
+  const { values } = colorObj;
+  const h = values[0];
+  const s = values[1] / 100;
+  const l = values[2] / 100;
+  const a = s * Math.min(l, 1 - l);
+  const f = (n: number, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k 
- 3, 9 - k, 1), -1);
+
+  if (colorObj.type === 'hsla') {
+    return recomposeColor({
+      type: 'rgba', values: [
+        Math.round(f(0) * 255),
+        Math.round(f(8) * 255),
+        Math.round(f(4) * 255),
+        colorObj.values[3]
+      ]
+    })
+  }
+
+  return recomposeColor({
+    type: 'rgb', values: [
+      Math.round(f(0) * 255),
+      Math.round(f(8) * 255),
+      Math.round(f(4) * 255)]
+  });
+}
+/**
+ * The relative brightness of any point in a color space,
+ * normalized to 0 for darkest black and 1 for lightest white.
+ *
+ * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla(), color()
+ * @returns {number} The relative brightness of the color in the range 0 - 1
+ */
+export function getLuminance(color: string): number {
+  const colorObj = decomposeColor(color);
+
+  const rgb2 = colorObj.type === 'hsl' ? 
decomposeColor(hslToRgb(color)).values : colorObj.values;
+  const rgb = rgb2.map((val) => {
+    val /= 255; // normalized
+    return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
+  }) as typeof rgb2;
+
+  // Truncate at 3 digits
+  return Number((0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * 
rgb[2]).toFixed(3));
+}
+
+/**
+ * Calculates the contrast ratio between two colors.
+ *
+ * Formula: https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
+ * @param {string} foreground - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla()
+ * @param {string} background - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla()
+ * @returns {number} A contrast ratio value in the range 0 - 21.
+ */
+export function getContrastRatio(foreground: string, background: string): 
number {
+  const lumA = getLuminance(foreground);
+  const lumB = getLuminance(background);
+  return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
+}
+
+/**
+ * Sets the absolute transparency of a color.
+ * Any existing alpha values are overwritten.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla(), color()
+ * @param {number} value - value to set the alpha channel to in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function alpha(color: string, value: number): string {
+  const colorObj = decomposeColor(color);
+  value = clamp(value);
+
+  if (colorObj.type === 'rgb' || colorObj.type === 'hsl') {
+    colorObj.type += 'a';
+  }
+  colorObj.values[3] = value;
+
+  return recomposeColor(colorObj);
+}
+
+/**
+ * Darkens a color.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function darken(color: string, coefficient: number): string {
+  const colorObj = decomposeColor(color);
+  coefficient = clamp(coefficient);
+
+  if (colorObj.type.indexOf('hsl') !== -1) {
+    colorObj.values[2] *= 1 - coefficient;
+  } else if (colorObj.type.indexOf('rgb') !== -1 || 
colorObj.type.indexOf('color') !== -1) {
+    for (let i = 0; i < 3; i += 1) {
+      colorObj.values[i] *= 1 - coefficient;
+    }
+  }
+  return recomposeColor(colorObj);
+}
+
+/**
+ * Lightens a color.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function lighten(color: string, coefficient: number): string {
+  const colorObj = decomposeColor(color);
+  coefficient = clamp(coefficient);
+
+  if (colorObj.type.indexOf('hsl') !== -1) {
+    colorObj.values[2] += (100 - colorObj.values[2]) * coefficient;
+  } else if (colorObj.type.indexOf('rgb') !== -1) {
+    for (let i = 0; i < 3; i += 1) {
+      colorObj.values[i] += (255 - colorObj.values[i]) * coefficient;
+    }
+  } else if (colorObj.type.indexOf('color') !== -1) {
+    for (let i = 0; i < 3; i += 1) {
+      colorObj.values[i] += (1 - colorObj.values[i]) * coefficient;
+    }
+  }
+
+  return recomposeColor(colorObj);
+}
+
+/**
+ * Darken or lighten a color, depending on its luminance.
+ * Light colors are darkened, dark colors are lightened.
+ * @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), 
rgba(), hsl(), hsla(), color()
+ * @param {number} coefficient=0.15 - multiplier in the range 0 - 1
+ * @returns {string} A CSS color string. Hex input values are returned as rgb
+ */
+export function emphasize(color: string, coefficient: number = 0.15): string {
+  return getLuminance(color) > 0.5 ? darken(color, coefficient) : 
lighten(color, coefficient);
+}
diff --git a/packages/taler-wallet-webextension/src/mui/style.tsx 
b/packages/taler-wallet-webextension/src/mui/style.tsx
new file mode 100644
index 00000000..84b0538b
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/style.tsx
@@ -0,0 +1,696 @@
+import { css } from "@linaria/core";
+import { darken, lighten } from "polished";
+import {
+  common,
+  purple,
+  red,
+  orange,
+  blue,
+  lightBlue,
+  green,
+  grey,
+} from "./colors/constants";
+import { getContrastRatio } from "./colors/manipulation";
+
+export function round(value: number): number {
+  return Math.round(value * 1e5) / 1e5;
+}
+const fontSize = 14;
+const htmlFontSize = 16;
+const coef = fontSize / 14;
+export function pxToRem(size: number): string {
+  return `${(size / htmlFontSize) * coef}rem`;
+}
+
+export const theme = createTheme();
+
+export const ripple = css`
+  background-position: center;
+
+  transition: background 0.5s;
+  &:hover {
+    background: #47a7f5 radial-gradient(circle, transparent 1%, #47a7f5 1%)
+      center/15000%;
+  }
+  &:active {
+    background-color: #6eb9f7;
+    background-size: 100%;
+    transition: background 0s;
+  }
+`;
+
+function createTheme() {
+  const light = {
+    // The colors used to style the text.
+    text: {
+      // The most important text.
+      primary: "rgba(0, 0, 0, 0.87)",
+      // Secondary text.
+      secondary: "rgba(0, 0, 0, 0.6)",
+      // Disabled text have even lower visual prominence.
+      disabled: "rgba(0, 0, 0, 0.38)",
+    },
+    // The color used to divide different elements.
+    divider: "rgba(0, 0, 0, 0.12)",
+    // The background colors used to style the surfaces.
+    // Consistency between these values is important.
+    background: {
+      paper: common.white,
+      default: common.white,
+    },
+    // The colors used to style the action elements.
+    action: {
+      // The color of an active action like an icon button.
+      active: "rgba(0, 0, 0, 0.54)",
+      // The color of an hovered action.
+      hover: "rgba(0, 0, 0, 0.04)",
+      hoverOpacity: 0.04,
+      // The color of a selected action.
+      selected: "rgba(0, 0, 0, 0.08)",
+      selectedOpacity: 0.08,
+      // The color of a disabled action.
+      disabled: "rgba(0, 0, 0, 0.26)",
+      // The background color of a disabled action.
+      disabledBackground: "rgba(0, 0, 0, 0.12)",
+      disabledOpacity: 0.38,
+      focus: "rgba(0, 0, 0, 0.12)",
+      focusOpacity: 0.12,
+      activatedOpacity: 0.12,
+    },
+  };
+
+  const dark = {
+    text: {
+      primary: common.white,
+      secondary: "rgba(255, 255, 255, 0.7)",
+      disabled: "rgba(255, 255, 255, 0.5)",
+      icon: "rgba(255, 255, 255, 0.5)",
+    },
+    divider: "rgba(255, 255, 255, 0.12)",
+    background: {
+      paper: "#121212",
+      default: "#121212",
+    },
+    action: {
+      active: common.white,
+      hover: "rgba(255, 255, 255, 0.08)",
+      hoverOpacity: 0.08,
+      selected: "rgba(255, 255, 255, 0.16)",
+      selectedOpacity: 0.16,
+      disabled: "rgba(255, 255, 255, 0.3)",
+      disabledBackground: "rgba(255, 255, 255, 0.12)",
+      disabledOpacity: 0.38,
+      focus: "rgba(255, 255, 255, 0.12)",
+      focusOpacity: 0.12,
+      activatedOpacity: 0.24,
+    },
+  };
+
+  const defaultFontFamily = '"Roboto", "Helvetica", "Arial", sans-serif';
+
+  const shadowKeyUmbraOpacity = 0.2;
+  const shadowKeyPenumbraOpacity = 0.14;
+  const shadowAmbientShadowOpacity = 0.12;
+
+  const typography = createTypography({});
+  const palette = createPalette({});
+  const shadows = createAllShadows();
+  const transitions = createTransitions({});
+  const breakpoints = createBreakpoints({});
+  const shape = {
+    borderRadius: css`
+      border-radius: 4px;
+    `,
+  };
+  /////////////////////
+  ///////////////////// BREAKPOINTS
+  /////////////////////
+  function createBreakpoints(breakpoints: any) {
+    const {
+      // The breakpoint **start** at this value.
+      // For instance with the first breakpoint xs: [xs, sm).
+      values = {
+        xs: 0,
+        sm: 600,
+        md: 900,
+        lg: 1200,
+        xl: 1536, // large screen
+      },
+      unit = "px",
+      step = 5,
+      // ...other
+    } = breakpoints;
+
+    const keys = Object.keys(values);
+
+    function up(key: any) {
+      const value = typeof values[key] === "number" ? values[key] : key;
+      return `@media (min-width:${value}${unit})`;
+    }
+
+    function down(key: any) {
+      const value = typeof values[key] === "number" ? values[key] : key;
+      return `@media (max-width:${value - step / 100}${unit})`;
+    }
+
+    function between(start: any, end: any) {
+      const endIndex = keys.indexOf(end);
+
+      return (
+        `@media (min-width:${
+          typeof values[start] === "number" ? values[start] : start
+        }${unit}) and ` +
+        `(max-width:${
+          (endIndex !== -1 && typeof values[keys[endIndex]] === "number"
+            ? values[keys[endIndex]]
+            : end) -
+          step / 100
+        }${unit})`
+      );
+    }
+
+    function only(key: any) {
+      if (keys.indexOf(key) + 1 < keys.length) {
+        return between(key, keys[keys.indexOf(key) + 1]);
+      }
+
+      return up(key);
+    }
+
+    function not(key: any) {
+      // handle first and last key separately, for better readability
+      const keyIndex = keys.indexOf(key);
+      if (keyIndex === 0) {
+        return up(keys[1]);
+      }
+      if (keyIndex === keys.length - 1) {
+        return down(keys[keyIndex]);
+      }
+
+      return between(key, keys[keys.indexOf(key) + 1]).replace(
+        "@media",
+        "@media not all and",
+      );
+    }
+
+    return {
+      keys,
+      values,
+      up,
+      down,
+      between,
+      only,
+      not,
+      unit,
+      // ...other,
+    };
+  }
+
+  /////////////////////
+  ///////////////////// SHADOWS
+  /////////////////////
+  function createShadow(...px: number[]): string {
+    return [
+      `${px[0]}px ${px[1]}px ${px[2]}px ${px[3]}px 
rgba(0,0,0,${shadowKeyUmbraOpacity})`,
+      `${px[4]}px ${px[5]}px ${px[6]}px ${px[7]}px 
rgba(0,0,0,${shadowKeyPenumbraOpacity})`,
+      `${px[8]}px ${px[9]}px ${px[10]}px ${px[11]}px 
rgba(0,0,0,${shadowAmbientShadowOpacity})`,
+    ].join(",");
+  }
+
+  function createAllShadows() {
+    // Values from 
https://github.com/material-components/material-components-web/blob/be8747f94574669cb5e7add1a7c54fa41a89cec7/packages/mdc-elevation/_variables.scss
+    return [
+      "none",
+      createShadow(0, 2, 1, -1, 0, 1, 1, 0, 0, 1, 3, 0),
+      createShadow(0, 3, 1, -2, 0, 2, 2, 0, 0, 1, 5, 0),
+      createShadow(0, 3, 3, -2, 0, 3, 4, 0, 0, 1, 8, 0),
+      createShadow(0, 2, 4, -1, 0, 4, 5, 0, 0, 1, 10, 0),
+      createShadow(0, 3, 5, -1, 0, 5, 8, 0, 0, 1, 14, 0),
+      createShadow(0, 3, 5, -1, 0, 6, 10, 0, 0, 1, 18, 0),
+      createShadow(0, 4, 5, -2, 0, 7, 10, 1, 0, 2, 16, 1),
+      createShadow(0, 5, 5, -3, 0, 8, 10, 1, 0, 3, 14, 2),
+      createShadow(0, 5, 6, -3, 0, 9, 12, 1, 0, 3, 16, 2),
+      createShadow(0, 6, 6, -3, 0, 10, 14, 1, 0, 4, 18, 3),
+      createShadow(0, 6, 7, -4, 0, 11, 15, 1, 0, 4, 20, 3),
+      createShadow(0, 7, 8, -4, 0, 12, 17, 2, 0, 5, 22, 4),
+      createShadow(0, 7, 8, -4, 0, 13, 19, 2, 0, 5, 24, 4),
+      createShadow(0, 7, 9, -4, 0, 14, 21, 2, 0, 5, 26, 4),
+      createShadow(0, 8, 9, -5, 0, 15, 22, 2, 0, 6, 28, 5),
+      createShadow(0, 8, 10, -5, 0, 16, 24, 2, 0, 6, 30, 5),
+      createShadow(0, 8, 11, -5, 0, 17, 26, 2, 0, 6, 32, 5),
+      createShadow(0, 9, 11, -5, 0, 18, 28, 2, 0, 7, 34, 6),
+      createShadow(0, 9, 12, -6, 0, 19, 29, 2, 0, 7, 36, 6),
+      createShadow(0, 10, 13, -6, 0, 20, 31, 3, 0, 8, 38, 7),
+      createShadow(0, 10, 13, -6, 0, 21, 33, 3, 0, 8, 40, 7),
+      createShadow(0, 10, 14, -6, 0, 22, 35, 3, 0, 8, 42, 7),
+      createShadow(0, 11, 14, -7, 0, 23, 36, 3, 0, 9, 44, 8),
+      createShadow(0, 11, 15, -7, 0, 24, 38, 3, 0, 9, 46, 8),
+    ];
+  }
+
+  /////////////////////
+  ///////////////////// TYPOGRAPHY
+  /////////////////////
+  /**
+   * @see @link{https://material.io/design/typography/the-type-system.html}
+   * @see 
@link{https://material.io/design/typography/understanding-typography.html}
+   */
+  function createTypography(typography: any) {
+    // const {
+    const fontFamily = defaultFontFamily,
+      // The default font size of the Material Specification.
+      fontSize = 14, // px
+      fontWeightLight = 300,
+      fontWeightRegular = 400,
+      fontWeightMedium = 500,
+      fontWeightBold = 700,
+      // Tell MUI what's the font-size on the html element.
+      // 16px is the default font-size used by browsers.
+      htmlFontSize = 16;
+    // Apply the CSS properties to all the variants.
+    // allVariants,
+    // pxToRem: pxToRem2,
+    // ...other
+    // } = typography;
+    const variants = {
+      // h1: buildVariant(fontWeightLight, 96, 1.167, -1.5),
+      // h2: buildVariant(fontWeightLight, 60, 1.2, -0.5),
+      // h3: buildVariant(fontWeightRegular, 48, 1.167, 0),
+      // h4: buildVariant(fontWeightRegular, 34, 1.235, 0.25),
+      // h5: buildVariant(fontWeightRegular, 24, 1.334, 0),
+      // h6: buildVariant(fontWeightMedium, 20, 1.6, 0.15),
+      // subtitle1: buildVariant(fontWeightRegular, 16, 1.75, 0.15),
+      // subtitle2: buildVariant(fontWeightMedium, 14, 1.57, 0.1),
+      // body1: buildVariant(fontWeightRegular, 16, 1.5, 0.15),
+      // body2: buildVariant(fontWeightRegular, 14, 1.43, 0.15),
+      button: css`
+        font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+        font-weight: ${fontWeightMedium};
+        font-size: ${pxToRem(14)};
+        line-height: 1.75;
+        letter-spacing: ${round(0.4 / 14)}em;
+        text-transform: uppercase;
+      `,
+      // button: buildVariant(fontWeightMedium, 14, 1.75, 0.4, caseAllCaps),
+      // caption: buildVariant(fontWeightRegular, 12, 1.66, 0.4),
+      // overline: buildVariant(fontWeightRegular, 12, 2.66, 1, caseAllCaps),
+    };
+
+    return deepmerge(
+      {
+        htmlFontSize,
+        pxToRem,
+        fontFamily,
+        fontSize,
+        fontWeightLight,
+        fontWeightRegular,
+        fontWeightMedium,
+        fontWeightBold,
+        ...variants,
+      },
+      // other,
+      {
+        clone: false, // No need to clone deep
+      },
+    );
+  }
+
+  /////////////////////
+  ///////////////////// MIXINS
+  /////////////////////
+  function createMixins(breakpoints: any, spacing: any, mixins: any) {
+    return {
+      toolbar: {
+        minHeight: 56,
+        [`${breakpoints.up("xs")} and (orientation: landscape)`]: {
+          minHeight: 48,
+        },
+        [breakpoints.up("sm")]: {
+          minHeight: 64,
+        },
+      },
+      ...mixins,
+    };
+  }
+
+  /////////////////////
+  ///////////////////// TRANSITION
+  /////////////////////
+  function formatMs(milliseconds: number) {
+    return `${Math.round(milliseconds)}ms`;
+  }
+
+  function getAutoHeightDuration(height: number) {
+    if (!height) {
+      return 0;
+    }
+
+    const constant = height / 36;
+
+    // 
https://www.wolframalpha.com/input/?i=(4+%2B+15+*+(x+%2F+36+)+**+0.25+%2B+(x+%2F+36)+%2F+5)+*+10
+    return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
+  }
+
+  function createTransitions(inputTransitions: any) {
+    // Follow 
https://material.google.com/motion/duration-easing.html#duration-easing-natural-easing-curves
+    // to learn the context in which each easing should be used.
+    const easing = {
+      // This is the most common easing curve.
+      easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
+      // Objects enter the screen at full velocity from off-screen and
+      // slowly decelerate to a resting point.
+      easeOut: "cubic-bezier(0.0, 0, 0.2, 1)",
+      // Objects leave the screen at full velocity. They do not decelerate 
when off-screen.
+      easeIn: "cubic-bezier(0.4, 0, 1, 1)",
+      // The sharp curve is used by objects that may return to the screen at 
any time.
+      sharp: "cubic-bezier(0.4, 0, 0.6, 1)",
+    };
+
+    // Follow 
https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations
+    // to learn when use what timing
+    const duration = {
+      shortest: 150,
+      shorter: 200,
+      short: 250,
+      // most basic recommended timing
+      standard: 300,
+      // this is to be used in complex animations
+      complex: 375,
+      // recommended when something is entering screen
+      enteringScreen: 225,
+      // recommended when something is leaving screen
+      leavingScreen: 195,
+    };
+
+    const mergedEasing = {
+      ...easing,
+      ...inputTransitions.easing,
+    };
+
+    const mergedDuration = {
+      ...duration,
+      ...inputTransitions.duration,
+    };
+
+    const create = (props = ["all"], options = {} as any) => {
+      const {
+        duration: durationOption = mergedDuration.standard,
+        easing: easingOption = mergedEasing.easeInOut,
+        delay = 0,
+        // ...other
+      } = options;
+
+      return (Array.isArray(props) ? props : [props])
+        .map(
+          (animatedProp) =>
+            `${animatedProp} ${
+              typeof durationOption === "string"
+                ? durationOption
+                : formatMs(durationOption)
+            } ${easingOption} ${
+              typeof delay === "string" ? delay : formatMs(delay)
+            }`,
+        )
+        .join(",");
+    };
+
+    return {
+      getAutoHeightDuration,
+      create,
+      ...inputTransitions,
+      easing: mergedEasing,
+      duration: mergedDuration,
+    };
+  }
+
+  /////////////////////
+  ///////////////////// PALETTE
+  /////////////////////
+  function createPalette(palette: any) {
+    // const {
+    const mode: "light" | "dark" = "light";
+    const contrastThreshold = 3;
+    const tonalOffset = 0.2;
+    // ...other
+    // } = palette;
+
+    const primary = palette.primary || getDefaultPrimary(mode);
+    const secondary = palette.secondary || getDefaultSecondary(mode);
+    const error = palette.error || getDefaultError(mode);
+    const info = palette.info || getDefaultInfo(mode);
+    const success = palette.success || getDefaultSuccess(mode);
+    const warning = palette.warning || getDefaultWarning(mode);
+
+    // Use the same logic as
+    // Bootstrap: 
https://github.com/twbs/bootstrap/blob/1d6e3710dd447de1a200f29e8fa521f8a0908f70/scss/_functions.scss#L59
+    // and material-components-web 
https://github.com/material-components/material-components-web/blob/ac46b8863c4dab9fc22c4c662dc6bd1b65dd652f/packages/mdc-theme/_functions.scss#L54
+    function getContrastText(background: string): string {
+      const contrastText =
+        getContrastRatio(background, dark.text.primary) >= contrastThreshold
+          ? dark.text.primary
+          : light.text.primary;
+
+      return contrastText;
+    }
+
+    const augmentColor = ({
+      color,
+      name,
+      mainShade = 500,
+      lightShade = 300,
+      darkShade = 700,
+    }: any) => {
+      color = { ...color };
+      if (!color.main && color[mainShade]) {
+        color.main = color[mainShade];
+      }
+
+      addLightOrDark(color, "light", lightShade, tonalOffset);
+      addLightOrDark(color, "dark", darkShade, tonalOffset);
+      if (!color.contrastText) {
+        color.contrastText = getContrastText(color.main);
+      }
+
+      return color;
+    };
+
+    const modes = { dark, light };
+
+    // if (process.env.NODE_ENV !== "production") {
+    //   if (!modes[mode]) {
+    //     console.error(`MUI: The palette mode \`${mode}\` is not 
supported.`);
+    //   }
+    // }
+    const paletteOutput = deepmerge(
+      {
+        // A collection of common colors.
+        common,
+        // The palette mode, can be light or dark.
+        mode,
+        // The colors used to represent primary interface elements for a user.
+        primary: augmentColor({ color: primary, name: "primary" }),
+        // The colors used to represent secondary interface elements for a 
user.
+        secondary: augmentColor({
+          color: secondary,
+          name: "secondary",
+          mainShade: "A400",
+          lightShade: "A200",
+          darkShade: "A700",
+        }),
+        // The colors used to represent interface elements that the user 
should be made aware of.
+        error: augmentColor({ color: error, name: "error" }),
+        // The colors used to represent potentially dangerous actions or 
important messages.
+        warning: augmentColor({ color: warning, name: "warning" }),
+        // The colors used to present information to the user that is neutral 
and not necessarily important.
+        info: augmentColor({ color: info, name: "info" }),
+        // The colors used to indicate the successful completion of an action 
that user triggered.
+        success: augmentColor({ color: success, name: "success" }),
+        // The grey colors.
+        grey,
+        // Used by `getContrastText()` to maximize the contrast between
+        // the background and the text.
+        contrastThreshold,
+        // Takes a background color and returns the text color that maximizes 
the contrast.
+        getContrastText,
+        // Generate a rich color object.
+        augmentColor,
+        // Used by the functions below to shift a color's luminance by 
approximately
+        // two indexes within its tonal palette.
+        // E.g., shift from Red 500 to Red 300 or Red 700.
+        tonalOffset,
+        // The light and dark mode object.
+        ...modes[mode],
+      },
+      // other:
+      {},
+    );
+
+    return paletteOutput;
+  }
+
+  function addLightOrDark(
+    intent: any,
+    direction: any,
+    shade: any,
+    tonalOffset: any,
+  ): void {
+    const tonalOffsetLight = tonalOffset.light || tonalOffset;
+    const tonalOffsetDark = tonalOffset.dark || tonalOffset * 1.5;
+
+    if (!intent[direction]) {
+      if (intent.hasOwnProperty(shade)) {
+        intent[direction] = intent[shade];
+      } else if (direction === "light") {
+        intent.light = lighten(intent.main, tonalOffsetLight);
+      } else if (direction === "dark") {
+        intent.dark = darken(intent.main, tonalOffsetDark);
+      }
+    }
+  }
+
+  function getDefaultPrimary(mode = "light") {
+    if (mode === "dark") {
+      return {
+        main: blue[200],
+        light: blue[50],
+        dark: blue[400],
+      };
+    }
+    return {
+      main: blue[700],
+      light: blue[400],
+      dark: blue[800],
+    };
+  }
+
+  function getDefaultSecondary(mode = "light") {
+    if (mode === "dark") {
+      return {
+        main: purple[200],
+        light: purple[50],
+        dark: purple[400],
+      };
+    }
+    return {
+      main: purple[500],
+      light: purple[300],
+      dark: purple[700],
+    };
+  }
+
+  function getDefaultError(mode = "light") {
+    if (mode === "dark") {
+      return {
+        main: red[500],
+        light: red[300],
+        dark: red[700],
+      };
+    }
+    return {
+      main: red[700],
+      light: red[400],
+      dark: red[800],
+    };
+  }
+
+  function getDefaultInfo(mode = "light") {
+    if (mode === "dark") {
+      return {
+        main: lightBlue[400],
+        light: lightBlue[300],
+        dark: lightBlue[700],
+      };
+    }
+    return {
+      main: lightBlue[700],
+      light: lightBlue[500],
+      dark: lightBlue[900],
+    };
+  }
+
+  function getDefaultSuccess(mode = "light") {
+    if (mode === "dark") {
+      return {
+        main: green[400],
+        light: green[300],
+        dark: green[700],
+      };
+    }
+    return {
+      main: green[800],
+      light: green[500],
+      dark: green[900],
+    };
+  }
+
+  function getDefaultWarning(mode = "light") {
+    if (mode === "dark") {
+      return {
+        main: orange[400],
+        light: orange[300],
+        dark: orange[700],
+      };
+    }
+    return {
+      main: "#ed6c02",
+      light: orange[500],
+      dark: orange[900],
+    };
+  }
+
+  /////////////////////
+  ///////////////////// DEEP MERGE
+  /////////////////////
+  function isPlainObject(item: unknown): item is Record<keyof any, unknown> {
+    return (
+      item !== null && typeof item === "object" && item.constructor === Object
+    );
+  }
+
+  interface DeepmergeOptions {
+    clone?: boolean;
+  }
+
+  function deepmerge<T>(
+    target: T,
+    source: unknown,
+    options: DeepmergeOptions = { clone: true },
+  ): T {
+    const output = options.clone ? { ...target } : target;
+
+    if (isPlainObject(target) && isPlainObject(source)) {
+      Object.keys(source).forEach((key) => {
+        // Avoid prototype pollution
+        if (key === "__proto__") {
+          return;
+        }
+
+        if (
+          isPlainObject(source[key]) &&
+          key in target &&
+          isPlainObject(target[key])
+        ) {
+          // Since `output` is a clone of `target` and we have narrowed 
`target` in this block we can cast to the same type.
+          (output as Record<keyof any, unknown>)[key] = deepmerge(
+            target[key],
+            source[key],
+            options,
+          );
+        } else {
+          (output as Record<keyof any, unknown>)[key] = source[key];
+        }
+      });
+    }
+
+    return output;
+  }
+  return {
+    typography,
+    palette,
+    shadows,
+    shape,
+    transitions,
+    breakpoints,
+    pxToRem,
+  };
+}
diff --git 
a/packages/taler-wallet-webextension/src/popup/DeveloperPage.stories.tsx 
b/packages/taler-wallet-webextension/src/popup/DeveloperPage.stories.tsx
index fb117725..4dcfe231 100644
--- a/packages/taler-wallet-webextension/src/popup/DeveloperPage.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/DeveloperPage.stories.tsx
@@ -45,4 +45,5 @@ export const AllOff = createExample(TestedComponent, {
       retryInfo: undefined,
     },
   ],
+  coins: [],
 });
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
index 2b9e60cb..6e490fdf 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
@@ -86,5 +86,3 @@ export const BitcoinTest = createExample(TestedComponent, {
   },
   exchangeBaseUrl: "https://exchange.demo.taler.net";,
 });
-// tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx OK
-// tb10v8ahvcqqleage3q5rqn3agnr7pd25msd5wd4hcj
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx 
b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
index 66e9cd21..5a54c2e4 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
@@ -27,7 +27,6 @@ export function ReserveCreated({
   amount,
 }: Props): VNode {
   const paytoURI = parsePaytoUri(payto);
-  // const url = new URL(paytoURI?.targetPath);
   if (!paytoURI) {
     return (
       <div>
diff --git a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx 
b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
index 37ad97af..7b28cb74 100644
--- a/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Welcome.tsx
@@ -61,7 +61,6 @@ export function View({
         <p>
           <i18n.Translate>Thank you for installing the wallet.</i18n.Translate>
         </p>
-        <Diagnostics diagnostics={diagnostics} timedOut={timedOut} />
         <h2>
           <i18n.Translate>Permissions</i18n.Translate>
         </h2>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9fd68c66..fd7f1021 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -345,16 +345,19 @@ importers:
       '@storybook/preact': 6.4.18
       '@testing-library/preact': ^2.0.1
       '@testing-library/preact-hooks': ^1.1.0
+      '@types/chai': ^4.3.0
       '@types/chrome': 0.0.176
       '@types/history': ^4.7.8
       '@types/mocha': ^9.0.0
       '@types/node': ^17.0.8
       babel-loader: ^8.2.3
       babel-plugin-transform-react-jsx: ^6.24.1
+      chai: ^4.3.6
       date-fns: ^2.28.0
       history: 4.10.1
       mocha: ^9.2.0
       nyc: ^15.1.0
+      polished: ^4.1.4
       preact: ^10.6.5
       preact-cli: ^3.3.5
       preact-render-to-string: ^5.1.19
@@ -371,8 +374,11 @@ importers:
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
       '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
+      '@types/chai': 4.3.0
+      chai: 4.3.6
       date-fns: 2.28.0
       history: 4.10.1
+      polished: 4.1.4
       preact: 10.6.5
       preact-router: 3.2.1_preact@10.6.5
       qrcode-generator: 1.4.4
@@ -2992,7 +2998,6 @@ packages:
     engines: {node: '>=6.9.0'}
     dependencies:
       regenerator-runtime: 0.13.9
-    dev: true
 
   /@babel/template/7.14.5:
     resolution: {integrity: 
sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==}
@@ -6667,6 +6672,10 @@ packages:
       '@types/node': 17.0.17
     dev: true
 
+  /@types/chai/4.3.0:
+    resolution: {integrity: 
sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==}
+    dev: false
+
   /@types/cheerio/0.22.30:
     resolution: {integrity: 
sha512-t7ZVArWZlq3dFa9Yt33qFBQIK4CQd1Q3UJp0V+UhP6vgLWLM6Qug7vZuRSGXg45zXeB1Fm5X2vmBkEX58LV2Tw==}
     dependencies:
@@ -7734,6 +7743,10 @@ packages:
       util: 0.10.3
     dev: true
 
+  /assertion-error/1.1.0:
+    resolution: {integrity: 
sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+    dev: false
+
   /assign-symbols/1.0.0:
     resolution: {integrity: sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=}
     engines: {node: '>=0.10.0'}
@@ -8747,6 +8760,19 @@ packages:
     resolution: {integrity: 
sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
     dev: true
 
+  /chai/4.3.6:
+    resolution: {integrity: 
sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==}
+    engines: {node: '>=4'}
+    dependencies:
+      assertion-error: 1.1.0
+      check-error: 1.0.2
+      deep-eql: 3.0.1
+      get-func-name: 2.0.0
+      loupe: 2.3.4
+      pathval: 1.1.1
+      type-detect: 4.0.8
+    dev: false
+
   /chalk/0.4.0:
     resolution: {integrity: sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=}
     engines: {node: '>=0.8.0'}
@@ -8812,6 +8838,10 @@ packages:
     engines: {node: '>=6'}
     dev: true
 
+  /check-error/1.0.2:
+    resolution: {integrity: sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=}
+    dev: false
+
   /cheerio-select/1.5.0:
     resolution: {integrity: 
sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==}
     dependencies:
@@ -9933,6 +9963,13 @@ packages:
       mimic-response: 1.0.1
     dev: true
 
+  /deep-eql/3.0.1:
+    resolution: {integrity: 
sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==}
+    engines: {node: '>=0.12'}
+    dependencies:
+      type-detect: 4.0.8
+    dev: false
+
   /deep-equal/1.1.1:
     resolution: {integrity: 
sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==}
     dependencies:
@@ -11843,6 +11880,10 @@ packages:
     engines: {node: 6.* || 8.* || >= 10.*}
     dev: true
 
+  /get-func-name/2.0.0:
+    resolution: {integrity: sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=}
+    dev: false
+
   /get-intrinsic/1.1.1:
     resolution: {integrity: 
sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==}
     dependencies:
@@ -13795,6 +13836,12 @@ packages:
     dependencies:
       js-tokens: 4.0.0
 
+  /loupe/2.3.4:
+    resolution: {integrity: 
sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==}
+    dependencies:
+      get-func-name: 2.0.0
+    dev: false
+
   /lower-case/1.1.4:
     resolution: {integrity: sha1-miyr0bno4K6ZOkv31YdcOcQujqw=}
     dev: true
@@ -15188,6 +15235,10 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /pathval/1.1.1:
+    resolution: {integrity: 
sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+    dev: false
+
   /pbkdf2/3.1.2:
     resolution: {integrity: 
sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
     engines: {node: '>=0.12'}
@@ -15308,7 +15359,6 @@ packages:
     engines: {node: '>=10'}
     dependencies:
       '@babel/runtime': 7.17.2
-    dev: true
 
   /portfinder/1.0.28:
     resolution: {integrity: 
sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==}
@@ -18840,7 +18890,6 @@ packages:
   /type-detect/4.0.8:
     resolution: {integrity: 
sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
     engines: {node: '>=4'}
-    dev: true
 
   /type-fest/0.13.1:
     resolution: {integrity: 
sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]