diff options
author | Sebastian <sebasjm@gmail.com> | 2022-03-18 17:52:46 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2022-03-18 17:52:46 -0300 |
commit | 65eb64cd07dcaf1b57405189fcd054684d3f5e2f (patch) | |
tree | 4d10faf8f975bbccceb2286ce2eb00a5000bbbbc | |
parent | 98761a2b8d50b1547ed1230f7c462ed205656c77 (diff) |
mui text field, standard variation
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 |