diff options
Diffstat (limited to 'packages')
21 files changed, 1356 insertions, 186 deletions
| diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 03232dee7..18563312c 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -37,9 +37,6 @@      "@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", @@ -57,14 +54,17 @@      "@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",      "mocha": "^9.2.0",      "nyc": "^15.1.0", +    "polished": "^4.1.4",      "preact-cli": "^3.3.5",      "preact-render-to-string": "^5.1.19",      "rimraf": "^3.0.2", diff --git a/packages/taler-wallet-webextension/src/mui/Button.stories.tsx b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx new file mode 100644 index 000000000..a6863add3 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/Button.stories.tsx @@ -0,0 +1,133 @@ +/* + 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 { Button } from "./Button"; +import { Fragment, h } from "preact"; +import DeleteIcon from "../../static/img/delete_24px.svg"; +import SendIcon from "../../static/img/send_24px.svg"; +import { styled } from "@linaria/react"; + +export default { +  title: "mui/button", +  component: Button, +}; + +const Stack = styled.div` +  display: flex; +  flex-direction: column; +`; + +export const BasicExample = () => ( +  <Fragment> +    <Stack> +      <Button size="small" variant="text"> +        Text +      </Button> +      <Button size="small" variant="contained"> +        Contained +      </Button> +      <Button size="small" variant="outlined"> +        Outlined +      </Button> +    </Stack> +    <Stack> +      <Button variant="text">Text</Button> +      <Button variant="contained">Contained</Button> +      <Button variant="outlined">Outlined</Button> +    </Stack> +    <Stack> +      <Button size="large" variant="text"> +        Text +      </Button> +      <Button size="large" variant="contained"> +        Contained +      </Button> +      <Button size="large" variant="outlined"> +        Outlined +      </Button> +    </Stack> +  </Fragment> +); + +export const Others = () => ( +  <Fragment> +    <p>colors</p> +    <Stack> +      <Button color="secondary">Secondary</Button> +      <Button variant="contained" color="success"> +        Success +      </Button> +      <Button variant="outlined" color="error"> +        Error +      </Button> +    </Stack> +    <p>disabled</p> +    <Stack> +      <Button disabled variant="text"> +        Text +      </Button> +      <Button disabled variant="contained"> +        Contained +      </Button> +      <Button disabled variant="outlined"> +        Outlined +      </Button> +    </Stack> +  </Fragment> +); + +export const WithIcons = () => ( +  <Fragment> +    <Stack> +      <Button variant="outlined" size="small" startIcon={DeleteIcon}> +        Delete +      </Button> +      <Button variant="contained" size="small" endIcon={SendIcon}> +        Send +      </Button> +      <Button variant="text" size="small" endIcon={SendIcon}> +        Send +      </Button> +    </Stack> +    <Stack> +      <Button variant="outlined" startIcon={DeleteIcon}> +        Delete +      </Button> +      <Button variant="contained" endIcon={SendIcon}> +        Send +      </Button> +      <Button variant="text" endIcon={SendIcon}> +        Send +      </Button> +    </Stack> +    <Stack> +      <Button variant="outlined" size="large" startIcon={DeleteIcon}> +        Delete +      </Button> +      <Button variant="contained" size="large" endIcon={SendIcon}> +        Send +      </Button> +      <Button variant="text" size="large" endIcon={SendIcon}> +        Send +      </Button> +    </Stack> +  </Fragment> +); diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx index ccca360fa..8da5b86be 100644 --- a/packages/taler-wallet-webextension/src/mui/Button.tsx +++ b/packages/taler-wallet-webextension/src/mui/Button.tsx @@ -1,6 +1,6 @@  import { ComponentChildren, h, VNode } from "preact";  import { css } from "@linaria/core"; -import { theme, ripple } from "./style"; +import { theme, ripple, Colors } from "./style";  import { alpha } from "./colors/manipulation";  interface Props { @@ -12,9 +12,9 @@ interface Props {    fullWidth?: boolean;    href?: string;    size?: "small" | "medium" | "large"; -  startIcon?: VNode; +  startIcon?: VNode | string;    variant?: "contained" | "outlined" | "text"; -  color?: "primary" | "secondary" | "success" | "error" | "info" | "warning"; +  color?: Colors;    onClick?: () => void;  } @@ -28,7 +28,7 @@ const baseStyle = css`    outline: 0;    border: 0;    margin: 0; -  border-radius: 0; +  /* border-radius: 0; */    padding: 0;    cursor: pointer;    user-select: none; @@ -50,6 +50,17 @@ const button = css`      color: ${theme.palette.action.disabled};    }  `; +const colorIconVariant = { +  outlined: css` +    background-color: var(--color-main); +  `, +  contained: css` +    background-color: var(--color-contrastText); +  `, +  text: css` +    background-color: var(--color-main); +  `, +};  const colorVariant = {    outlined: css` @@ -90,6 +101,47 @@ const colorVariant = {    `,  }; +const sizeIconVariant = { +  outlined: { +    small: css` +      padding: 3px; +      font-size: ${theme.pxToRem(7)}; +    `, +    medium: css` +      padding: 5px; +    `, +    large: css` +      padding: 7px; +      font-size: ${theme.pxToRem(10)}; +    `, +  }, +  contained: { +    small: css` +      padding: 4px; +      font-size: ${theme.pxToRem(13)}; +    `, +    medium: css` +      padding: 6px; +    `, +    large: css` +      padding: 8px; +      font-size: ${theme.pxToRem(10)}; +    `, +  }, +  text: { +    small: css` +      padding: 4px; +      font-size: ${theme.pxToRem(13)}; +    `, +    medium: css` +      padding: 6px; +    `, +    large: css` +      padding: 8px; +      font-size: ${theme.pxToRem(15)}; +    `, +  }, +};  const sizeVariant = {    outlined: {      small: css` @@ -162,12 +214,18 @@ export function Button({          css`            margin-right: 8px;            margin-left: -4px; +          mask: var(--image) no-repeat center;          `, +        colorIconVariant[variant], +        sizeIconVariant[variant][size],          style,        ].join(" ")} -    > -      {sip} -    </span> +      style={{ +        "--image": `url("${sip}")`, +        "--color-main": theme.palette[color].main, +        "--color-contrastText": theme.palette[color].contrastText, +      }} +    />    );    const endIcon = eip && (      <span @@ -175,12 +233,19 @@ export function Button({          css`            margin-right: -4px;            margin-left: 8px; +          mask: var(--image) no-repeat center;          `, +        colorIconVariant[variant], +        sizeIconVariant[variant][size],          style,        ].join(" ")} -    > -      {eip} -    </span> +      style={{ +        "--image": `url("${eip}")`, +        "--color-main": theme.palette[color].main, +        "--color-contrastText": theme.palette[color].contrastText, +        "--color-dark": theme.palette[color].dark, +      }} +    />    );    return (      <button @@ -196,8 +261,8 @@ export function Button({        ].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-main-alpha-half": alpha(theme.palette[color].main, 0.5),          "--color-dark": theme.palette[color].dark,          "--color-main-alpha-opacity": alpha(            theme.palette[color].main, diff --git a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx new file mode 100644 index 000000000..a2f7e1e66 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx @@ -0,0 +1,108 @@ +/* + 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 { styled } from "@linaria/react"; +import { Fragment, h } from "preact"; +import { useState } from "preact/hooks"; +import { TextField, Props } from "./TextField"; + +export default { +  title: "mui/TextField", +  component: TextField, +}; + +const Container = styled.div` +  display: flex; +  flex-direction: column; +  & > * { +    margin: 20px; +  } +`; + +const BasicExample = (variant: Props["variant"]) => { +  const [value, onChange] = useState(""); +  return ( +    <Container> +      <TextField variant={variant} label="Name" {...{ value, onChange }} /> +      <TextField +        variant={variant} +        type="password" +        label="Password" +        {...{ value, onChange }} +      /> +      <TextField +        disabled +        variant={variant} +        label="Country" +        helperText="this is disabled" +        value="disabled" +      /> +      <TextField +        error +        variant={variant} +        label="Something" +        {...{ value, onChange }} +      /> +      <TextField +        error +        disabled +        variant={variant} +        label="Disabled and Error" +        value="disabled with error" +        helperText="this field has an error" +      /> +      <TextField +        variant={variant} +        required +        label="Name" +        {...{ value, onChange }} +        helperText="this field is required" +      /> +    </Container> +  ); +}; + +export const Standard = () => BasicExample("standard"); +export const Filled = () => BasicExample("filled"); +export const Outlined = () => BasicExample("outlined"); + +export const Color = () => ( +  <Container> +    <TextField +      variant="standard" +      label="Outlined secondary" +      color="secondary" +      focused +    /> +    <TextField +      label="Filled success" +      variant="standard" +      color="success" +      focused +    /> +    <TextField +      label="Standard warning" +      variant="standard" +      color="warning" +      focused +    /> +  </Container> +); diff --git a/packages/taler-wallet-webextension/src/mui/TextField.tsx b/packages/taler-wallet-webextension/src/mui/TextField.tsx new file mode 100644 index 000000000..ada8d5d85 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/TextField.tsx @@ -0,0 +1,69 @@ +import { ComponentChildren, h, VNode } from "preact"; +import { FormControl } from "./input/FormControl"; +import { FormHelperText } from "./input/FormHelperText"; +import { InputFilled } from "./input/InputFilled"; +import { InputLabel } from "./input/InputLabel"; +import { InputOutlined } from "./input/InputOutlined"; +import { InputStandard } from "./input/InputStandard"; +import { SelectFilled } from "./input/SelectFilled"; +import { SelectOutlined } from "./input/SelectOutlined"; +import { SelectStandard } from "./input/SelectStandard"; +import { Colors } from "./style"; + +export interface Props { +  autoComplete?: string; +  autoFocus?: boolean; +  color?: Colors; +  disabled?: boolean; +  error?: boolean; +  fullWidth?: boolean; +  helperText?: VNode | string; +  id?: string; +  label?: VNode | string; +  margin?: "dense" | "normal" | "none"; +  maxRows?: number; +  minRows?: number; +  multiline?: boolean; +  onChange?: (s: string) => void; +  placeholder?: string; +  required?: boolean; +  focused?: boolean; +  rows?: number; +  select?: boolean; +  type?: string; +  value?: string; +  variant?: "filled" | "outlined" | "standard"; +  children?: ComponentChildren; +} + +export function TextField({ +  label, +  select, +  helperText, +  children, +  variant = "standard", +  ...props +}: Props): VNode { +  // htmlFor={id} id={inputLabelId} +  const Input = select ? selectVariant[variant] : inputVariant[variant]; +  // console.log("variant", Input); +  return ( +    <FormControl {...props}> +      {label && <InputLabel>{label}</InputLabel>} +      <Input {...props}>{children}</Input> +      {helperText && <FormHelperText>{helperText}</FormHelperText>} +    </FormControl> +  ); +} + +const inputVariant = { +  standard: InputStandard, +  filled: InputFilled, +  outlined: InputOutlined, +}; + +const selectVariant = { +  standard: SelectStandard, +  filled: SelectFilled, +  outlined: SelectOutlined, +}; diff --git a/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx new file mode 100644 index 000000000..7a8395705 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/FormControl.tsx @@ -0,0 +1,156 @@ +import { css } from "@linaria/core"; +import { ComponentChildren, createContext, h } from "preact"; +import { useContext, useState } from "preact/hooks"; +import { Colors } from "../style"; + +export interface Props { +  color: Colors; +  disabled: boolean; +  error: boolean; +  focused: boolean; +  fullWidth: boolean; +  hiddenLabel: boolean; +  required: boolean; +  variant: "filled" | "outlined" | "standard"; +  margin: "none" | "normal" | "dense"; +  size: "medium" | "small"; +  children: ComponentChildren; +} + +export const root = css` +  display: inline-flex; +  flex-direction: column; +  position: relative; +  min-width: 0px; +  padding: 0px; +  margin: 0px; +  border: 0px; +  vertical-align: top; +`; + +const marginVariant = { +  none: "", +  normal: css` +    margin-top: 16px; +    margin-bottom: 8px; +  `, +  dense: css` +    margin-top: 8px; +    margin-bottom: 4px; +  `, +}; +const fullWidthStyle = css` +  width: 100%; +`; + +export function FormControl({ +  color = "primary", +  disabled = false, +  error = false, +  focused: visuallyFocused, +  fullWidth = false, +  hiddenLabel = false, +  margin = "none", +  required = false, +  size = "medium", +  variant = "standard", +  children, +}: Partial<Props>) { +  const [filled, setFilled] = useState(false); +  const [focusedState, setFocused] = useState(false); +  const focused = +    visuallyFocused !== undefined && !disabled ? visuallyFocused : focusedState; + +  const value: FCCProps = { +    color, +    disabled, +    error, +    filled, +    focused, +    fullWidth, +    hiddenLabel, +    size, +    onBlur: () => { +      setFocused(false); +    }, +    onEmpty: () => { +      setFilled(false); +    }, +    onFilled: () => { +      setFilled(true); +    }, +    onFocus: () => { +      setFocused(true); +    }, +    required, +    variant, +  }; + +  return ( +    <div +      class={[ +        root, +        marginVariant[margin], +        fullWidth ? fullWidthStyle : "", +      ].join(" ")} +    > +      <FormControlContext.Provider value={value}> +        {children} +      </FormControlContext.Provider> +    </div> +  ); +} + +export interface FCCProps { +  // adornedStart, +  // setAdornedStart, +  color: Colors; +  disabled: boolean; +  error: boolean; +  filled: boolean; +  focused: boolean; +  fullWidth: boolean; +  hiddenLabel: boolean; +  size: "medium" | "small"; +  onBlur: () => void; +  onEmpty: () => void; +  onFilled: () => void; +  onFocus: () => void; +  // registerEffect, +  required: boolean; +  variant: "filled" | "outlined" | "standard"; +} + +export const FormControlContext = createContext<FCCProps | null>(null); + +const defaultContextValue: FCCProps = { +  color: "primary", +  disabled: false, +  error: false, +  filled: false, +  focused: false, +  fullWidth: false, +  hiddenLabel: false, +  size: "medium", +  onBlur: () => {}, +  onEmpty: () => {}, +  onFilled: () => {}, +  onFocus: () => {}, +  required: false, +  variant: "outlined", +}; + +function withoutUndefinedProperties(obj: any) { +  return Object.keys(obj).reduce((acc, key) => { +    const _acc: any = acc; +    if (obj[key] !== undefined) _acc[key] = obj[key]; +    return _acc; +  }, {}); +} + +export function useFormControl(props: Partial<FCCProps> = {}): FCCProps { +  const ctx = useContext(FormControlContext); +  const cleanedProps = withoutUndefinedProperties(props); +  if (!ctx) return { ...defaultContextValue, ...cleanedProps }; +  return { ...ctx, ...cleanedProps }; +} diff --git a/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx new file mode 100644 index 000000000..4854a6384 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/FormHelperText.tsx @@ -0,0 +1,54 @@ +import { css } from "@linaria/core"; +import { ComponentChildren, h } from "preact"; +import { theme } from "../style"; +import { useFormControl } from "./FormControl"; + +const root = css` +  color: ${theme.palette.text.secondary}; +  text-align: left; +  margin-top: 3px; +  margin-bottom: 0px; +  margin-right: 0px; +  margin-left: 0px; +`; +const disabledStyle = css` +  color: ${theme.palette.text.disabled}; +`; +const errorStyle = css` +  color: ${theme.palette.error.main}; +`; +const sizeSmallStyle = css` +  margin-top: 4px; +`; +const containedStyle = css` +  margin-right: 14px; +  margin-left: 14px; +`; + +interface Props { +  disabled?: boolean; +  error?: boolean; +  filled?: boolean; +  focused?: boolean; +  margin?: "dense"; +  required?: boolean; +  children: ComponentChildren; +} +export function FormHelperText({ children, ...props }: Props) { +  const fcs = useFormControl(props); +  const contained = fcs.variant === "filled" || fcs.variant === "outlined"; +  return ( +    <p +      class={[ +        root, +        theme.typography.caption, +        fcs.disabled && disabledStyle, +        fcs.error && errorStyle, +        fcs.size === "small" && sizeSmallStyle, +        contained && containedStyle, +      ].join(" ")} +    > +      {children} +    </p> +  ); +} diff --git a/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx b/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx new file mode 100644 index 000000000..e5ca53263 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/FormLabel.tsx @@ -0,0 +1,67 @@ +import { css } from "@linaria/core"; +import { ComponentChildren, h } from "preact"; +import { Colors, theme } from "../style"; +import { useFormControl } from "./FormControl"; + +export interface Props { +  class?: string; +  disabled?: boolean; +  error?: boolean; +  filled?: boolean; +  focused?: boolean; +  required?: boolean; +  color?: Colors; +  children?: ComponentChildren; +} + +const root = css` +  color: ${theme.palette.text.secondary}; +  line-height: 1.4375em; +  padding: 0px; +  position: relative; +  &[data-focused] { +    color: var(--color-main); +  } +  &[data-disabled] { +    color: ${theme.palette.text.disabled}; +  } +  &[data-error] { +    color: ${theme.palette.error.main}; +  } +`; + +export function FormLabel({ +  disabled, +  error, +  filled, +  focused, +  required, +  color, +  class: _class, +  children, +  ...rest +}: Props) { +  const fcs = useFormControl({ +    disabled, +    error, +    filled, +    focused, +    required, +    color, +  }); +  return ( +    <label +      data-focused={fcs.focused} +      data-error={fcs.error} +      data-disabled={fcs.disabled} +      class={[_class, root, theme.typography.body1].join(" ")} +      {...rest} +      style={{ +        "--color-main": theme.palette[fcs.color].main, +      }} +    > +      {children} +      {fcs.required && <span data-error={fcs.error}> {"*"}</span>} +    </label> +  ); +} diff --git a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx new file mode 100644 index 000000000..5714eb1ba --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx @@ -0,0 +1,258 @@ +import { css } from "@linaria/core"; +import { h, JSX } from "preact"; +import { useEffect, useLayoutEffect, useState } from "preact/hooks"; +import { theme } from "../style"; +import { FormControlContext, useFormControl } from "./FormControl"; + +const rootStyle = css` +  color: ${theme.palette.text.primary}; +  line-height: 1.4375em; +  box-sizing: border-box; +  position: relative; +  cursor: text; +  display: inline-flex; +  align-items: center; +`; +const rootDisabledStyle = css` +  color: ${theme.palette.text.disabled}; +  cursor: default; +`; +const rootMultilineStyle = css` +  padding: 4px 0 5px; +`; +const fullWidthStyle = css` +  width: "100%"; +`; + +export function InputBaseRoot({ +  class: _class, +  disabled, +  error, +  multiline, +  focused, +  fullWidth, +  children, +}: any) { +  const fcs = useFormControl({}); +  return ( +    <div +      data-disabled={disabled} +      data-focused={focused} +      data-error={error} +      class={[ +        _class, +        rootStyle, +        theme.typography.body1, +        disabled && rootDisabledStyle, +        multiline && rootMultilineStyle, +        fullWidth && fullWidthStyle, +      ].join(" ")} +      style={{ +        "--color-main": theme.palette[fcs.color].main, +      }} +    > +      {children} +    </div> +  ); +} + +const componentStyle = css` +  font: inherit; +  letter-spacing: inherit; +  color: currentColor; +  padding: 4px 0 5px; +  border: 0px; +  box-sizing: content-box; +  background: none; +  height: 1.4375em; +  margin: 0px; +  -webkit-tap-highlight-color: transparent; +  display: block; +  min-width: 0px; +  width: 100%; +  animation-name: "auto-fill-cancel"; +  animation-duration: 10ms; + +  @keyframes auto-fill { +    from { +      display: block; +    } +  } +  @keyframes auto-fill-cancel { +    from { +      display: block; +    } +  } +  &::placeholder { +    color: "currentColor"; +    opacity: ${theme.palette.mode === "light" ? 0.42 : 0.5}; +    transition: ${theme.transitions.create("opacity", { +      duration: theme.transitions.duration.shorter, +    })}; +  } +  &:focus { +    outline: 0; +  } +  &:invalid { +    box-shadow: none; +  } +  &::-webkit-search-decoration { +    -webkit-appearance: none; +  } +  &:-webkit-autofill { +    animation-duration: 5000s; +    animation-name: auto-fill; +  } +`; +const componentDisabledStyle = css` +  opacity: 1; +  --webkit-text-fill-color: ${theme.palette.text.disabled}; +`; +const componentSmallStyle = css` +  padding-top: 1px; +`; +const componentMultilineStyle = css` +  height: auto; +  resize: none; +  padding: 0px; +  padding-top: 0px; +`; +const searchStyle = css` +  -moz-appearance: textfield; +  -webkit-appearance: textfield; +`; + +export function InputBaseComponent({ +  disabled, +  size, +  multiline, +  type, +  ...props +}: any) { +  return ( +    <input +      disabled={disabled} +      type={type} +      class={[ +        componentStyle, +        disabled && componentDisabledStyle, +        size === "small" && componentSmallStyle, +        multiline && componentMultilineStyle, +        type === "search" && searchStyle, +      ].join(" ")} +      {...props} +    /> +  ); +} + +export function InputBase({ +  Root = InputBaseRoot, +  Input, +  onChange, +  name, +  placeholder, +  readOnly, +  onKeyUp, +  onKeyDown, +  rows, +  type = "text", +  value, +  onClick, +  ...props +}: any) { +  const fcs = useFormControl(props); +  // const [focused, setFocused] = useState(false); +  useLayoutEffect(() => { +    if (value && value !== "") { +      fcs.onFilled(); +    } else { +      fcs.onEmpty(); +    } +  }, [value]); + +  const handleFocus = (event: JSX.TargetedFocusEvent<EventTarget>) => { +    // Fix a bug with IE11 where the focus/blur events are triggered +    // while the component is disabled. +    if (fcs.disabled) { +      event.stopPropagation(); +      return; +    } + +    // if (onFocus) { +    //   onFocus(event); +    // } +    // if (inputPropsProp.onFocus) { +    //   inputPropsProp.onFocus(event); +    // } + +    fcs.onFocus(); +  }; + +  const handleBlur = () => { +    // if (onBlur) { +    //   onBlur(event); +    // } +    // if (inputPropsProp.onBlur) { +    //   inputPropsProp.onBlur(event); +    // } + +    fcs.onBlur(); +  }; + +  const handleChange = ( +    event: JSX.TargetedEvent<HTMLElement & { value?: string }>, +  ) => { +    // if (inputPropsProp.onChange) { +    //   inputPropsProp.onChange(event, ...args); +    // } + +    // Perform in the willUpdate +    if (onChange) { +      onChange(event.currentTarget.value); +    } +  }; + +  const handleClick = ( +    event: JSX.TargetedMouseEvent<HTMLElement & { value?: string }>, +  ) => { +    // if (inputRef.current && event.currentTarget === event.target) { +    //   inputRef.current.focus(); +    // } + +    if (onClick) { +      onClick(event.currentTarget.value); +    } +  }; + +  if (!Input) { +    Input = props.multiline ? TextareaAutoSize : InputBaseComponent; +  } + +  return ( +    <Root {...fcs} onClick={handleClick}> +      <FormControlContext.Provider value={null}> +        <Input +          aria-invalid={fcs.error} +          // aria-describedby={} +          disabled={fcs.disabled} +          name={name} +          placeholder={placeholder} +          readOnly={readOnly} +          required={fcs.required} +          rows={rows} +          value={value} +          onKeyDown={onKeyDown} +          onKeyUp={onKeyUp} +          type={type} +          onChange={handleChange} +          onBlur={handleBlur} +          onFocus={handleFocus} +        /> +      </FormControlContext.Provider> +    </Root> +  ); +} + +export function TextareaAutoSize() { +  return <input onClick={(e) => null} />; +} diff --git a/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx new file mode 100644 index 000000000..5c50a8b72 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/InputFilled.tsx @@ -0,0 +1,5 @@ +import { h, VNode } from "preact"; + +export function InputFilled(): VNode { +  return <div />; +} diff --git a/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx b/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx new file mode 100644 index 000000000..c70c5bfc0 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/InputLabel.tsx @@ -0,0 +1,98 @@ +import { css } from "@linaria/core"; +import { ComponentChildren, h } from "preact"; +import { Colors, theme } from "../style"; +import { useFormControl } from "./FormControl"; +import { FormLabel } from "./FormLabel"; + +const root = css` +  display: block; +  transform-origin: top left; +  white-space: nowrap; +  overflow: hidden; +  text-overflow: ellipsis; +  max-width: 100%; + +  &[data-form-control] { +    position: absolute; +    left: 0px; +    top: 0px; +    transform: translate(0, 20px) scale(1); +  } +  &[data-size="small"] { +    transform: translate(0, 17px) scale(1); +  } +  &[data-shrink] { +    transform: translate(0, -1.5px) scale(0.75); +    transform-origin: top left; +    max-width: 133%; +  } +  &:not([data-disable-animation]) { +    transition: ${theme.transitions.create( +      ["color", "transform", "max-width"], +      { +        duration: theme.transitions.duration.shorter, +        easing: theme.transitions.easing.easeOut, +      }, +    )}; +  } +  &[data-variant="filled"] { +    z-index: 1; +    pointer-events: none; +    transform: translate(12px, 16px) scale(1); +    max-width: calc(100% - 24px); +    &[data-size="small"] { +      transform: translate(12px, 13px) scale(1); +    } +    &[data-shrink] { +      user-select: none; +      pointer-events: auto; +      transform: translate(12px, 7px) scale(0.75); +      max-width: calc(133% - 24px); +      &[data-size="small"] { +        transform: translate(12px, 4px) scale(0.75); +      } +    } +  } +  &[data-variant="outlined"] { +    z-index: 1; +    pointer-events: none; +    transform: translate(14px, 16px) scale(1); +    max-width: calc(100% - 24px); +    &[data-size="small"] { +      transform: translate(14px, 9px) scale(1); +    } +    &[data-shrink] { +      user-select: none; +      pointer-events: auto; +      transform: translate(14px, -9px) scale(0.75); +      max-width: calc(133% - 24px); +    } +  } +`; + +interface InputLabelProps { +  color: Colors; +  disableAnimation: boolean; +  disabled: boolean; +  error: boolean; +  focused: boolean; +  margin: boolean; +  required: boolean; +  shrink: boolean; +  variant: "filled" | "outlined" | "standard"; +  children: ComponentChildren; +} +export function InputLabel(props: Partial<InputLabelProps>) { +  const fcs = useFormControl(props); +  return ( +    <FormLabel +      data-form-control={!!fcs} +      data-size={fcs.size} +      data-shrink={props.shrink || fcs.filled || fcs.focused} +      data-disable-animation={props.disableAnimation} +      data-variant={fcs.variant} +      class={root} +      {...props} +    /> +  ); +} diff --git a/packages/taler-wallet-webextension/src/mui/input/InputOutlined.tsx b/packages/taler-wallet-webextension/src/mui/input/InputOutlined.tsx new file mode 100644 index 000000000..3b40ffc70 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/InputOutlined.tsx @@ -0,0 +1,5 @@ +import { h, VNode } from "preact"; + +export function InputOutlined(): VNode { +  return <div />; +} diff --git a/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx new file mode 100644 index 000000000..ba5145719 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/InputStandard.tsx @@ -0,0 +1,124 @@ +import { css } from "@linaria/core"; +import { h, VNode } from "preact"; +import { Colors, theme } from "../style"; +import { useFormControl } from "./FormControl"; +import { InputBase, InputBaseComponent, InputBaseRoot } from "./InputBase"; + +export interface Props { +  autoComplete?: string; +  autoFocus?: boolean; +  color?: Colors; +  defaultValue?: string; +  disabled?: boolean; +  disableUnderline?: boolean; +  endAdornment?: VNode; +  error?: boolean; +  fullWidth?: boolean; +  id?: string; +  margin?: "dense" | "normal" | "none"; +  maxRows?: number; +  minRows?: number; +  multiline?: boolean; +  name?: string; +  onChange?: (s: string) => void; +  placeholder?: string; +  readOnly?: boolean; +  required?: boolean; +  rows?: number; +  startAdornment?: VNode; +  type?: string; +  value?: string; +} +export function InputStandard({ +  type = "text", +  multiline, +  ...props +}: Props): VNode { +  const fcs = useFormControl(props); +  return ( +    <InputBase +      Root={Root} +      Input={Input} +      fullWidth={fcs.fullWidth} +      multiline={multiline} +      type={type} +      {...props} +    /> +  ); +} + +const rootStyle = css` +  position: relative; +`; +const formControlStyle = css` +  label + & { +    margin-top: 16px; +  } +`; +const underlineStyle = css` +  &:after { +    border-bottom: 2px solid var(--color-main); +    left: 0px; +    bottom: 0px; +    content: ""; +    position: absolute; +    right: 0px; +    transform: scaleX(0); +    transition: ${theme.transitions.create("transform", { +      duration: theme.transitions.duration.shorter, +      easing: theme.transitions.easing.easeOut, +    })}; +    pointer-events: none; +  } +  &[data-focused]:after { +    transform: scaleX(1); +  } +  &[data-error]:after { +    border-bottom-color: ${theme.palette.error.main}; +    transform: scaleY(1); +  } +  &:before { +    border-bottom: 1px solid +      ${theme.palette.mode === "light" +        ? "rgba(0, 0, 0, 0.42)" +        : "rgba(255, 255, 255, 0.7)"}; +    left: 0px; +    bottom: 0px; +    right: 0px; +    content: "\\00a0"; +    position: absolute; +    transition: ${theme.transitions.create("border-bottom-color", { +      duration: theme.transitions.duration.shorter, +    })}; +    pointer-events: none; +  } +  &:hover:not([data-disabled]:before) { +    border-bottom: 2px solid var(--color-main); +    @media (hover: none) { +      border-bottom: 1px solid +        ${theme.palette.mode === "light" +          ? "rgba(0, 0, 0, 0.42)" +          : "rgba(255, 255, 255, 0.7)"}; +    } +  } +  &[data-disabled]:before { +    border-bottom-style: solid; +  } +`; + +function Root({ disabled, focused, error, children }: any) { +  return ( +    <InputBaseRoot +      disabled={disabled} +      focused={focused} +      error={error} +      class={[rootStyle, formControlStyle, underlineStyle].join(" ")} +    > +      {children} +    </InputBaseRoot> +  ); +} + +function Input(props: any) { +  return <InputBaseComponent {...props} />; +} diff --git a/packages/taler-wallet-webextension/src/mui/input/SelectFilled.tsx b/packages/taler-wallet-webextension/src/mui/input/SelectFilled.tsx new file mode 100644 index 000000000..28b1859f8 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/SelectFilled.tsx @@ -0,0 +1,5 @@ +import { h, VNode } from "preact"; + +export function SelectFilled(): VNode { +  return <div />; +} diff --git a/packages/taler-wallet-webextension/src/mui/input/SelectOutlined.tsx b/packages/taler-wallet-webextension/src/mui/input/SelectOutlined.tsx new file mode 100644 index 000000000..10ee4015c --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/SelectOutlined.tsx @@ -0,0 +1,5 @@ +import { h, VNode } from "preact"; + +export function SelectOutlined(): VNode { +  return <div />; +} diff --git a/packages/taler-wallet-webextension/src/mui/input/SelectStandard.tsx b/packages/taler-wallet-webextension/src/mui/input/SelectStandard.tsx new file mode 100644 index 000000000..72cb635df --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/input/SelectStandard.tsx @@ -0,0 +1,5 @@ +import { h, VNode } from "preact"; + +export function SelectStandard(): VNode { +  return <div />; +} diff --git a/packages/taler-wallet-webextension/src/mui/style.tsx b/packages/taler-wallet-webextension/src/mui/style.tsx index 5f9cd2244..3fa3b7e33 100644 --- a/packages/taler-wallet-webextension/src/mui/style.tsx +++ b/packages/taler-wallet-webextension/src/mui/style.tsx @@ -12,6 +12,14 @@ import {  } from "./colors/constants";  import { getContrastRatio } from "./colors/manipulation"; +export type Colors = +  | "primary" +  | "secondary" +  | "success" +  | "error" +  | "info" +  | "warning"; +  export function round(value: number): number {    return Math.round(value * 1e5) / 1e5;  } @@ -386,6 +394,14 @@ function createTheme() {        `,        /* just of caseAllCaps */        // button: buildVariant(fontWeightMedium, 14, 1.75, 0.4, caseAllCaps), + +      caption: css` +        font-family: "Roboto", "Helvetica", "Arial", sans-serif; +        font-weight: ${fontWeightMedium}; +        font-size: ${pxToRem(12)}; +        line-height: 1.66; +        letter-spacing: ${round(0.4 / 12)}em; +      `,        // caption: buildVariant(fontWeightRegular, 12, 1.66, 0.4),        // overline: buildVariant(fontWeightRegular, 12, 2.66, 1, caseAllCaps),      }; diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index f4df4f7f5..e78bc4ff9 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -57,7 +57,7 @@ function main(): void {    }  } -setupI18n("en-US", strings); +setupI18n("en", strings);  if (document.readyState === "loading") {    document.addEventListener("DOMContentLoaded", main); diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index a346df2c8..9a1d8699a 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -73,7 +73,7 @@ function main(): void {    }  } -setupI18n("en-US", strings); +setupI18n("en", strings);  if (document.readyState === "loading") {    document.addEventListener("DOMContentLoaded", main); @@ -102,188 +102,183 @@ function Application(): VNode {    return (      <TranslationProvider>        <DevContextProvider> -        {({ devMode }: { devMode: boolean }) => ( -          <IoCProviderForRuntime> -            {/* <Match/> won't work in the first render if <Router /> is not called first */} -            {/* https://github.com/preactjs/preact-router/issues/415 */} -            <Router history={hash_history} /> -            <Match> -              {({ path }: { path: string }) => { -                if (path && path.startsWith("/cta")) return; -                return ( -                  <Fragment> -                    <LogoHeader /> -                    <WalletNavBar path={path} /> -                  </Fragment> -                ); -              }} -            </Match> -            <div -              style={{ -                backgroundColor: "lightcyan", -                display: "flex", -                justifyContent: "center", -              }} +        <IoCProviderForRuntime> +          {/* <Match/> won't work in the first render if <Router /> is not called first */} +          {/* https://github.com/preactjs/preact-router/issues/415 */} +          <Router history={hash_history} /> +          <Match> +            {({ path }: { path: string }) => { +              if (path && path.startsWith("/cta")) return; +              return ( +                <Fragment> +                  <LogoHeader /> +                  <WalletNavBar path={path} /> +                </Fragment> +              ); +            }} +          </Match> +          <div +            style={{ +              backgroundColor: "lightcyan", +              display: "flex", +              justifyContent: "center", +            }} +          > +            <PendingTransactions +              goToTransaction={(txId: string) => +                route(Pages.balance_transaction.replace(":tid", txId)) +              } +            /> +          </div> +          <WalletBox> +            {globalNotification && ( +              <SuccessBox onClick={clearNotification}> +                <div>{globalNotification}</div> +              </SuccessBox> +            )} +            <Router +              history={hash_history} +              onChange={clearNotificationWhenMovingOut}              > -              <PendingTransactions -                goToTransaction={(txId: string) => -                  route(Pages.balance_transaction.replace(":tid", txId)) -                } -              /> -            </div> -            <WalletBox> -              {globalNotification && ( -                <SuccessBox onClick={clearNotification}> -                  <div>{globalNotification}</div> -                </SuccessBox> -              )} -              <Router -                history={hash_history} -                onChange={clearNotificationWhenMovingOut} -              > -                <Route path={Pages.welcome} component={WelcomePage} /> +              <Route path={Pages.welcome} component={WelcomePage} /> -                {/** -                 * BALANCE -                 */} +              {/** +               * BALANCE +               */} -                <Route -                  path={Pages.balance_history} -                  component={HistoryPage} -                  goToWalletDeposit={(currency: string) => -                    route(Pages.balance_deposit.replace(":currency", currency)) -                  } -                  goToWalletManualWithdraw={(currency?: string) => -                    route( -                      Pages.balance_manual_withdraw.replace( -                        ":currency?", -                        currency || "", -                      ), -                    ) -                  } -                /> -                <Route -                  path={Pages.balance_transaction} -                  component={TransactionPage} -                  goToWalletHistory={(currency?: string) => { -                    route( -                      Pages.balance_history.replace( -                        ":currency", -                        currency || "", -                      ), -                    ); -                  }} -                /> +              <Route +                path={Pages.balance_history} +                component={HistoryPage} +                goToWalletDeposit={(currency: string) => +                  route(Pages.balance_deposit.replace(":currency", currency)) +                } +                goToWalletManualWithdraw={(currency?: string) => +                  route( +                    Pages.balance_manual_withdraw.replace( +                      ":currency?", +                      currency || "", +                    ), +                  ) +                } +              /> +              <Route +                path={Pages.balance_transaction} +                component={TransactionPage} +                goToWalletHistory={(currency?: string) => { +                  route( +                    Pages.balance_history.replace(":currency", currency || ""), +                  ); +                }} +              /> -                <Route -                  path={Pages.balance_manual_withdraw} -                  component={ManualWithdrawPage} -                  onCancel={() => { -                    route(Pages.balance); -                  }} -                /> +              <Route +                path={Pages.balance_manual_withdraw} +                component={ManualWithdrawPage} +                onCancel={() => { +                  route(Pages.balance); +                }} +              /> -                <Route -                  path={Pages.balance_deposit} -                  component={DepositPage} -                  onCancel={(currency: string) => { -                    route(Pages.balance_history.replace(":currency", currency)); -                  }} -                  onSuccess={(currency: string) => { -                    route(Pages.balance_history.replace(":currency", currency)); -                    setGlobalNotification( -                      <i18n.Translate> -                        All done, your transaction is in progress -                      </i18n.Translate>, -                    ); -                  }} -                /> -                {/** -                 * PENDING -                 */} -                <Route path={Pages.settings} component={SettingsPage} /> +              <Route +                path={Pages.balance_deposit} +                component={DepositPage} +                onCancel={(currency: string) => { +                  route(Pages.balance_history.replace(":currency", currency)); +                }} +                onSuccess={(currency: string) => { +                  route(Pages.balance_history.replace(":currency", currency)); +                  setGlobalNotification( +                    <i18n.Translate> +                      All done, your transaction is in progress +                    </i18n.Translate>, +                  ); +                }} +              /> +              {/** +               * PENDING +               */} +              <Route path={Pages.settings} component={SettingsPage} /> -                {/** -                 * BACKUP -                 */} -                <Route -                  path={Pages.backup} -                  component={BackupPage} -                  onAddProvider={() => { -                    route(Pages.backup_provider_add); -                  }} -                /> -                <Route -                  path={Pages.backup_provider_detail} -                  component={ProviderDetailPage} -                  onBack={() => { -                    route(Pages.backup); -                  }} -                /> -                <Route -                  path={Pages.backup_provider_add} -                  component={ProviderAddPage} -                  onBack={() => { -                    route(Pages.backup); -                  }} -                /> +              {/** +               * BACKUP +               */} +              <Route +                path={Pages.backup} +                component={BackupPage} +                onAddProvider={() => { +                  route(Pages.backup_provider_add); +                }} +              /> +              <Route +                path={Pages.backup_provider_detail} +                component={ProviderDetailPage} +                onBack={() => { +                  route(Pages.backup); +                }} +              /> +              <Route +                path={Pages.backup_provider_add} +                component={ProviderAddPage} +                onBack={() => { +                  route(Pages.backup); +                }} +              /> -                {/** -                 * SETTINGS -                 */} -                <Route -                  path={Pages.settings_exchange_add} -                  component={ExchangeAddPage} -                  onBack={() => { -                    route(Pages.balance); -                  }} -                /> +              {/** +               * SETTINGS +               */} +              <Route +                path={Pages.settings_exchange_add} +                component={ExchangeAddPage} +                onBack={() => { +                  route(Pages.balance); +                }} +              /> -                {/** -                 * DEV -                 */} +              {/** +               * DEV +               */} -                <Route path={Pages.dev} component={DeveloperPage} /> +              <Route path={Pages.dev} component={DeveloperPage} /> -                {/** -                 * CALL TO ACTION -                 */} -                <Route -                  path={Pages.cta_pay} -                  component={PayPage} -                  goToWalletManualWithdraw={(currency?: string) => -                    route( -                      Pages.balance_manual_withdraw.replace( -                        ":currency?", -                        currency || "", -                      ), -                    ) -                  } -                  goBack={() => route(Pages.balance)} -                /> -                <Route path={Pages.cta_refund} component={RefundPage} /> -                <Route path={Pages.cta_tips} component={TipPage} /> -                <Route path={Pages.cta_withdraw} component={WithdrawPage} /> +              {/** +               * CALL TO ACTION +               */} +              <Route +                path={Pages.cta_pay} +                component={PayPage} +                goToWalletManualWithdraw={(currency?: string) => +                  route( +                    Pages.balance_manual_withdraw.replace( +                      ":currency?", +                      currency || "", +                    ), +                  ) +                } +                goBack={() => route(Pages.balance)} +              /> +              <Route path={Pages.cta_refund} component={RefundPage} /> +              <Route path={Pages.cta_tips} component={TipPage} /> +              <Route path={Pages.cta_withdraw} component={WithdrawPage} /> -                {/** -                 * NOT FOUND -                 * all redirects should be at the end -                 */} -                <Route -                  path={Pages.balance} -                  component={Redirect} -                  to={Pages.balance_history.replace(":currency", "")} -                /> +              {/** +               * NOT FOUND +               * all redirects should be at the end +               */} +              <Route +                path={Pages.balance} +                component={Redirect} +                to={Pages.balance_history.replace(":currency", "")} +              /> -                <Route -                  default -                  component={Redirect} -                  to={Pages.balance_history.replace(":currency", "")} -                /> -              </Router> -            </WalletBox> -          </IoCProviderForRuntime> -        )} +              <Route +                default +                component={Redirect} +                to={Pages.balance_history.replace(":currency", "")} +              /> +            </Router> +          </WalletBox> +        </IoCProviderForRuntime>        </DevContextProvider>      </TranslationProvider>    ); diff --git a/packages/taler-wallet-webextension/static/img/delete_24px.svg b/packages/taler-wallet-webextension/static/img/delete_24px.svg new file mode 100644 index 000000000..0d0b74d16 --- /dev/null +++ b/packages/taler-wallet-webextension/static/img/delete_24px.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
\ No newline at end of file diff --git a/packages/taler-wallet-webextension/static/img/send_24px.svg b/packages/taler-wallet-webextension/static/img/send_24px.svg new file mode 100644 index 000000000..95fe7a4c6 --- /dev/null +++ b/packages/taler-wallet-webextension/static/img/send_24px.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
\ No newline at end of file | 
