first banner implementation with mui

This commit is contained in:
Sebastian 2022-03-09 14:00:02 -03:00
parent 6bc244cc1e
commit 1607c728bc
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
20 changed files with 2186 additions and 13 deletions

View File

@ -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">

View File

@ -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",

View File

@ -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>

View File

@ -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;

View File

@ -0,0 +1,5 @@
import { h, Fragment, VNode, ComponentChildren } from "preact";
export function Avatar({}: { children: ComponentChildren }): VNode {
return <Fragment />;
}

View File

@ -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>
);
}

View File

@ -0,0 +1,5 @@
import { h, Fragment, VNode } from "preact";
export function Divider(): VNode {
return <Fragment />;
}

View File

@ -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 />;
}

View File

@ -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>
);

View File

@ -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));
};

View File

@ -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>;
}

View File

@ -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',
};

View File

@ -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%)');
});
});
});

View File

@ -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);
}

View File

@ -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,
};
}

View File

@ -45,4 +45,5 @@ export const AllOff = createExample(TestedComponent, {
retryInfo: undefined,
},
],
coins: [],
});

View File

@ -86,5 +86,3 @@ export const BitcoinTest = createExample(TestedComponent, {
},
exchangeBaseUrl: "https://exchange.demo.taler.net",
});
// tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx OK
// tb10v8ahvcqqleage3q5rqn3agnr7pd25msd5wd4hcj

View File

@ -27,7 +27,6 @@ export function ReserveCreated({
amount,
}: Props): VNode {
const paytoURI = parsePaytoUri(payto);
// const url = new URL(paytoURI?.targetPath);
if (!paytoURI) {
return (
<div>

View File

@ -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>

View File

@ -345,16 +345,19 @@ importers:
'@storybook/preact': 6.4.18
'@testing-library/preact': ^2.0.1
'@testing-library/preact-hooks': ^1.1.0
'@types/chai': ^4.3.0
'@types/chrome': 0.0.176
'@types/history': ^4.7.8
'@types/mocha': ^9.0.0
'@types/node': ^17.0.8
babel-loader: ^8.2.3
babel-plugin-transform-react-jsx: ^6.24.1
chai: ^4.3.6
date-fns: ^2.28.0
history: 4.10.1
mocha: ^9.2.0
nyc: ^15.1.0
polished: ^4.1.4
preact: ^10.6.5
preact-cli: ^3.3.5
preact-render-to-string: ^5.1.19
@ -371,8 +374,11 @@ importers:
dependencies:
'@gnu-taler/taler-util': link:../taler-util
'@gnu-taler/taler-wallet-core': link:../taler-wallet-core
'@types/chai': 4.3.0
chai: 4.3.6
date-fns: 2.28.0
history: 4.10.1
polished: 4.1.4
preact: 10.6.5
preact-router: 3.2.1_preact@10.6.5
qrcode-generator: 1.4.4
@ -2992,7 +2998,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.9
dev: true
/@babel/template/7.14.5:
resolution: {integrity: sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==}
@ -6667,6 +6672,10 @@ packages:
'@types/node': 17.0.17
dev: true
/@types/chai/4.3.0:
resolution: {integrity: sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==}
dev: false
/@types/cheerio/0.22.30:
resolution: {integrity: sha512-t7ZVArWZlq3dFa9Yt33qFBQIK4CQd1Q3UJp0V+UhP6vgLWLM6Qug7vZuRSGXg45zXeB1Fm5X2vmBkEX58LV2Tw==}
dependencies:
@ -7734,6 +7743,10 @@ packages:
util: 0.10.3
dev: true
/assertion-error/1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: false
/assign-symbols/1.0.0:
resolution: {integrity: sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=}
engines: {node: '>=0.10.0'}
@ -8747,6 +8760,19 @@ packages:
resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==}
dev: true
/chai/4.3.6:
resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==}
engines: {node: '>=4'}
dependencies:
assertion-error: 1.1.0
check-error: 1.0.2
deep-eql: 3.0.1
get-func-name: 2.0.0
loupe: 2.3.4
pathval: 1.1.1
type-detect: 4.0.8
dev: false
/chalk/0.4.0:
resolution: {integrity: sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=}
engines: {node: '>=0.8.0'}
@ -8812,6 +8838,10 @@ packages:
engines: {node: '>=6'}
dev: true
/check-error/1.0.2:
resolution: {integrity: sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=}
dev: false
/cheerio-select/1.5.0:
resolution: {integrity: sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==}
dependencies:
@ -9933,6 +9963,13 @@ packages:
mimic-response: 1.0.1
dev: true
/deep-eql/3.0.1:
resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==}
engines: {node: '>=0.12'}
dependencies:
type-detect: 4.0.8
dev: false
/deep-equal/1.1.1:
resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==}
dependencies:
@ -11843,6 +11880,10 @@ packages:
engines: {node: 6.* || 8.* || >= 10.*}
dev: true
/get-func-name/2.0.0:
resolution: {integrity: sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=}
dev: false
/get-intrinsic/1.1.1:
resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==}
dependencies:
@ -13795,6 +13836,12 @@ packages:
dependencies:
js-tokens: 4.0.0
/loupe/2.3.4:
resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==}
dependencies:
get-func-name: 2.0.0
dev: false
/lower-case/1.1.4:
resolution: {integrity: sha1-miyr0bno4K6ZOkv31YdcOcQujqw=}
dev: true
@ -15188,6 +15235,10 @@ packages:
engines: {node: '>=8'}
dev: true
/pathval/1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
dev: false
/pbkdf2/3.1.2:
resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
engines: {node: '>=0.12'}
@ -15308,7 +15359,6 @@ packages:
engines: {node: '>=10'}
dependencies:
'@babel/runtime': 7.17.2
dev: true
/portfinder/1.0.28:
resolution: {integrity: sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==}
@ -18840,7 +18890,6 @@ packages:
/type-detect/4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
dev: true
/type-fest/0.13.1:
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}