summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/mui
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-03-09 14:00:02 -0300
committerSebastian <sebasjm@gmail.com>2022-03-09 14:00:02 -0300
commit1607c728bca19a003ca08b64b4d2afc73e4d1e2a (patch)
treea02fc28342cf343cf91b182955cc903915989478 /packages/taler-wallet-webextension/src/mui
parent6bc244cc1e0e08c4d86161fdf2b6a52b3f9f452e (diff)
downloadwallet-core-1607c728bca19a003ca08b64b4d2afc73e4d1e2a.tar.gz
wallet-core-1607c728bca19a003ca08b64b4d2afc73e4d1e2a.tar.bz2
wallet-core-1607c728bca19a003ca08b64b4d2afc73e4d1e2a.zip
first banner implementation with mui
Diffstat (limited to 'packages/taler-wallet-webextension/src/mui')
-rw-r--r--packages/taler-wallet-webextension/src/mui/Avatar.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/mui/Button.tsx215
-rw-r--r--packages/taler-wallet-webextension/src/mui/Divider.tsx5
-rw-r--r--packages/taler-wallet-webextension/src/mui/Grid.tsx13
-rw-r--r--packages/taler-wallet-webextension/src/mui/Paper.stories.tsx149
-rw-r--r--packages/taler-wallet-webextension/src/mui/Paper.tsx63
-rw-r--r--packages/taler-wallet-webextension/src/mui/Typography.tsx9
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/constants.ts348
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/manipulation.test.ts305
-rw-r--r--packages/taler-wallet-webextension/src/mui/colors/manipulation.ts273
-rw-r--r--packages/taler-wallet-webextension/src/mui/style.tsx696
11 files changed, 2081 insertions, 0 deletions
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 000000000..963984ab6
--- /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 000000000..b197ca26a
--- /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 000000000..27ab392fc
--- /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 000000000..3974e3c2e
--- /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 000000000..f263526f2
--- /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 000000000..52524380b
--- /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 000000000..4fc614463
--- /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 000000000..a6e58caa9
--- /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 000000000..77b3ec884
--- /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 000000000..633c80c94
--- /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 000000000..84b0538be
--- /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,
+ };
+}