diff options
Diffstat (limited to 'packages')
19 files changed, 2134 insertions, 10 deletions
diff --git a/packages/taler-wallet-webextension/.storybook/preview.js b/packages/taler-wallet-webextension/.storybook/preview.js index 61484b665..9c1365d3a 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 6641f7dc5..c64f7f095 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 f72d54ef1..14619473b 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 000000000..6ff7b1019 --- /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 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, + }; +} diff --git a/packages/taler-wallet-webextension/src/popup/DeveloperPage.stories.tsx b/packages/taler-wallet-webextension/src/popup/DeveloperPage.stories.tsx index fb1177251..4dcfe2316 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 2b9e60cb5..6e490fdfd 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 66e9cd21e..5a54c2e41 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 37ad97afd..7b28cb742 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> |