From 2150f3d96b25772dd608e245cd3508f857478c5b Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 10 Mar 2022 23:13:10 -0300 Subject: grid implementation --- .../src/components/Banner.stories.tsx | 54 +++++ .../src/components/Banner.tsx | 49 ++++- .../src/components/styled/index.tsx | 10 + .../taler-wallet-webextension/src/mui/Avatar.tsx | 52 ++++- .../taler-wallet-webextension/src/mui/Button.tsx | 2 +- .../src/mui/Grid.stories.tsx | 192 ++++++++++++++++ .../taler-wallet-webextension/src/mui/Grid.tsx | 244 ++++++++++++++++++++- .../taler-wallet-webextension/src/mui/Paper.tsx | 2 +- .../src/mui/Typography.tsx | 89 +++++++- .../taler-wallet-webextension/src/mui/style.tsx | 78 ++++++- 10 files changed, 744 insertions(+), 28 deletions(-) create mode 100644 packages/taler-wallet-webextension/src/components/Banner.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/mui/Grid.stories.tsx (limited to 'packages/taler-wallet-webextension') diff --git a/packages/taler-wallet-webextension/src/components/Banner.stories.tsx b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx new file mode 100644 index 000000000..136302166 --- /dev/null +++ b/packages/taler-wallet-webextension/src/components/Banner.stories.tsx @@ -0,0 +1,54 @@ +/* + 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Banner } from "./Banner"; +import { Fragment, h } from "preact"; + +export default { + title: "mui/banner", + component: Banner, +}; + +function Wrapper({ children }: any) { + return ( +
+
{children}
+
+ ); +} + +export const BasicExample = () => ( + + + + + +); diff --git a/packages/taler-wallet-webextension/src/components/Banner.tsx b/packages/taler-wallet-webextension/src/components/Banner.tsx index 6ff7b1019..f6af81184 100644 --- a/packages/taler-wallet-webextension/src/components/Banner.tsx +++ b/packages/taler-wallet-webextension/src/components/Banner.tsx @@ -1,33 +1,58 @@ import { h, Fragment, VNode } from "preact"; import { Divider } from "../mui/Divider"; -import { Button } from "./styled/index.js"; +import { Button } from "../mui/Button"; import { Typography } from "../mui/Typography"; import { Avatar } from "../mui/Avatar"; import { Grid } from "../mui/Grid"; import { Paper } from "../mui/Paper"; +import { Icon } from "./styled"; +import settingsIcon from "../../static/img/settings_black_24dp.svg"; +// & > a > div.settings-icon { +// mask: url(${settingsIcon}) no-repeat center; +// background-color: white; +// width: 24px; +// height: 24px; +// margin-left: auto; +// margin-right: 8px; +// padding: 4px; +// } +// & > a.active { +// background-color: #f8faf7; +// color: #0042b2; +// font-weight: bold; +// } +// & > a.active > div.settings-icon { +// background-color: #0042b2; +// } -function SignalWifiOffIcon(): VNode { - return ; +function SignalWifiOffIcon({ ...rest }: any): VNode { + return ; } -function Banner({}: {}) { +export function Banner({}: {}) { return ( - - - - - + + + + + - + You have lost connection to the internet. This app is offline. - - + + diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index a5ed64a83..80bfaa549 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -826,6 +826,16 @@ export const NavigationHeader = styled.div` } `; +export const Icon = styled.div` + mask: url(${settingsIcon}) no-repeat center; + background-color: gray; + width: 24px; + height: 24px; + margin-left: auto; + margin-right: 8px; + padding: 4px; +`; + const image = `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`; export const NiceSelect = styled.div` diff --git a/packages/taler-wallet-webextension/src/mui/Avatar.tsx b/packages/taler-wallet-webextension/src/mui/Avatar.tsx index 963984ab6..d5bd9d421 100644 --- a/packages/taler-wallet-webextension/src/mui/Avatar.tsx +++ b/packages/taler-wallet-webextension/src/mui/Avatar.tsx @@ -1,5 +1,53 @@ +import { css } from "@linaria/core"; import { h, Fragment, VNode, ComponentChildren } from "preact"; +import { theme } from "./style"; -export function Avatar({}: { children: ComponentChildren }): VNode { - return ; +const root = css` + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 40px; + height: 40px; + font-family: ${theme.typography.fontFamily}; + font-size: ${theme.typography.pxToRem(20)}; + line-height: 1; + overflow: hidden; + user-select: none; +`; + +const colorStyle = css` + color: ${theme.palette.background.default}; + background-color: ${theme.palette.mode === "light" + ? theme.palette.grey[400] + : theme.palette.grey[600]}; +`; + +const avatarImageStyle = css` + width: 100%; + height: 100%; + text-align: center; + object-fit: cover; + color: transparent; + text-indent: 10000; +`; + +interface Props { + variant?: "circular" | "rounded" | "square"; + children?: ComponentChildren; +} + +export function Avatar({ variant, children, ...rest }: Props): VNode { + const borderStyle = + variant === "square" + ? theme.shape.squareBorder + : variant === "rounded" + ? theme.shape.roundBorder + : theme.shape.circularBorder; + return ( +
+ {children} +
+ ); } diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx index b197ca26a..f3272a57b 100644 --- a/packages/taler-wallet-webextension/src/mui/Button.tsx +++ b/packages/taler-wallet-webextension/src/mui/Button.tsx @@ -185,7 +185,7 @@ export function Button({ disabled={disabled} class={[ theme.typography.button, - theme.shape.borderRadius, + theme.shape.roundBorder, ripple, baseStyle, button, diff --git a/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx b/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx new file mode 100644 index 000000000..3c9361326 --- /dev/null +++ b/packages/taler-wallet-webextension/src/mui/Grid.stories.tsx @@ -0,0 +1,192 @@ +/* + 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 + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { Grid } from "./Grid"; +import { Fragment, h } from "preact"; + +export default { + title: "mui/grid", + component: Grid, +}; + +function Item({ children }: any) { + return ( +
+ {children} +
+ ); +} + +function Wrapper({ children }: any) { + return ( +
+
{children}
+
+ ); +} + +export const BasicExample = () => ( + + + + + xs=8 + + + xs=4 + + + xs=4 + + + xs=8 + + + + + + + xs=6 md=8 + + + xs=6 md=4 + + + xs=6 md=4 + + + xs=6 md=8 + + + + +); + +export const Responsive12ColumnsSize = () => ( + + +

Item size is responsive: xs=6 sm=4 md=2

+ + {Array.from(Array(6)).map((_, index) => ( + + item {index} + + ))} + +
+ +

Item size is fixed

+ + {Array.from(Array(6)).map((_, index) => ( + + item {index} + + ))} + +
+
+); + +export const Responsive12Spacing = () => ( + + +

Item space is responsive: xs=1 sm=2 md=3

+ + {Array.from(Array(6)).map((_, index) => ( + + item {index} + + ))} + +
+ +

Item space is fixed

+ + {Array.from(Array(6)).map((_, index) => ( + + item {index} + + ))} + +
+ + +

Item row space is responsive: xs=6 sm=4 md=1

+ + {Array.from(Array(6)).map((_, index) => ( + + item {index} + + ))} + +
+ +

Item col space is responsive: xs=6 sm=3 md=1

+ + {Array.from(Array(6)).map((_, index) => ( + + item {index} + + ))} + +
+
+); + +export const Example = () => ( + +

Item row space is responsive: xs=6 sm=4 md=1

+ + {Array.from(Array(6)).map((_, index) => ( + + item {index} + + ))} + +
+); diff --git a/packages/taler-wallet-webextension/src/mui/Grid.tsx b/packages/taler-wallet-webextension/src/mui/Grid.tsx index 3974e3c2e..ccabed060 100644 --- a/packages/taler-wallet-webextension/src/mui/Grid.tsx +++ b/packages/taler-wallet-webextension/src/mui/Grid.tsx @@ -1,13 +1,241 @@ -import { h, Fragment, VNode, ComponentChildren } from "preact"; +import { css } from "@linaria/core"; +import { h, Fragment, VNode, ComponentChildren, createContext } from "preact"; +import { useContext } from "preact/hooks"; +import { theme } from "./style"; -export function Grid({}: { +type ResponsiveKeys = "xs" | "sm" | "md" | "lg" | "xl"; + +export type ResponsiveSize = { + xs: number; + sm: number; + md: number; + lg: number; + xl: number; +}; + +const root = css` + box-sizing: border-box; +`; +const containerStyle = css` + display: flex; + flex-wrap: wrap; + width: 100%; +`; +const itemStyle = css` + margin: 0; +`; +const zeroMinWidthStyle = css` + min-width: 0px; +`; + +type GridSizes = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; +type SpacingSizes = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; + +export interface Props { + columns?: number | Partial; container?: boolean; - wrap?: string; item?: boolean; - spacing?: number; - alignItems?: string; - justify?: string; + + direction?: "column-reverse" | "column" | "row-reverse" | "row"; + + lg?: GridSizes | "auto" | "true"; + md?: GridSizes | "auto" | "true"; + sm?: GridSizes | "auto" | "true"; + xl?: GridSizes | "auto" | "true"; + xs?: GridSizes | "auto" | "true"; + + wrap?: "nowrap" | "wrap-reverse" | "wrap"; + spacing?: SpacingSizes | Partial; + columnSpacing?: SpacingSizes | Partial; + rowSpacing?: SpacingSizes | Partial; + + alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline"; + justifyContent?: + | "flex-start" + | "flex-end" + | "center" + | "space-around" + | "space-between" + | "space-evenly"; + + zeroMinWidth?: boolean; children: ComponentChildren; -}): VNode { - return ; +} +theme.breakpoints.up; + +function getOffset(val: number | string) { + if (typeof val === "number") `${val}px`; + return val; +} + +const columnGapVariant = css` + ${theme.breakpoints.up("xs")} { + width: calc(100% + var(--space-col-xs)); + margin-left: calc(-1 * var(--space-col-xs)); + & > div { + padding-left: var(--space-col-xs); + } + } + ${theme.breakpoints.up("sm")} { + width: calc(100% + var(--space-col-sm)); + margin-left: calc(-1 * var(--space-col-sm)); + & > div { + padding-left: var(--space-col-sm); + } + } + ${theme.breakpoints.up("md")} { + width: calc(100% + var(--space-col-md)); + margin-left: calc(-1 * var(--space-col-md)); + & > div { + padding-left: var(--space-col-md); + } + } +`; +const rowGapVariant = css` + ${theme.breakpoints.up("xs")} { + margin-top: calc(-1 * var(--space-row-xs)); + & > div { + padding-top: var(--space-row-xs); + } + } + ${theme.breakpoints.up("sm")} { + margin-top: calc(-1 * var(--space-row-sm)); + & > div { + padding-top: var(--space-row-sm); + } + } + ${theme.breakpoints.up("md")} { + margin-top: calc(-1 * var(--space-row-md)); + & > div { + padding-top: var(--space-row-md); + } + } +`; + +const sizeVariant = css` + ${theme.breakpoints.up("xs")} { + flex-basis: var(--relation-col-vs-xs); + flex-grow: 0; + max-width: var(--relation-col-vs-xs); + } + ${theme.breakpoints.up("sm")} { + flex-basis: var(--relation-col-vs-sm); + flex-grow: 0; + max-width: var(--relation-col-vs-sm); + } + ${theme.breakpoints.up("md")} { + flex-basis: var(--relation-col-vs-md); + flex-grow: 0; + max-width: var(--relation-col-vs-md); + } +`; + +const GridContext = createContext(toResponsive(12)); + +function toResponsive(v: number | Partial): ResponsiveSize { + const p = typeof v === "number" ? { xs: v } : v; + const xs = p.xs || 12; + const sm = p.sm || xs; + const md = p.md || sm; + const lg = p.lg || md; + const xl = p.xl || lg; + return { + xs, + sm, + md, + lg, + xl, + }; +} + +export function Grid({ + columns: cp, + container = false, + item = false, + direction = "row", + lg, + md, + sm, + xl, + xs, + wrap = "wrap", + spacing = 0, + columnSpacing: csp, + rowSpacing: rsp, + alignItems, + justifyContent, + zeroMinWidth = false, + children, +}: Props): VNode { + const cc = useContext(GridContext); + const columns = !cp ? cc : toResponsive(cp); + + const rowSpacing = rsp ? toResponsive(rsp) : toResponsive(spacing); + const columnSpacing = csp ? toResponsive(csp) : toResponsive(spacing); + + const ssize = toResponsive({ xs, md, lg, xl, sm } as any); + + if (container) { + console.log(rowSpacing); + console.log(columnSpacing); + } + const spacingStyles = !container + ? {} + : { + "--space-col-xs": getOffset(theme.spacing(columnSpacing.xs)), + "--space-col-sm": getOffset(theme.spacing(columnSpacing.sm)), + "--space-col-md": getOffset(theme.spacing(columnSpacing.md)), + "--space-col-lg": getOffset(theme.spacing(columnSpacing.lg)), + "--space-col-xl": getOffset(theme.spacing(columnSpacing.xl)), + + "--space-row-xs": getOffset(theme.spacing(rowSpacing.xs)), + "--space-row-sm": getOffset(theme.spacing(rowSpacing.sm)), + "--space-row-md": getOffset(theme.spacing(rowSpacing.md)), + "--space-row-lg": getOffset(theme.spacing(rowSpacing.lg)), + "--space-row-xl": getOffset(theme.spacing(rowSpacing.xl)), + }; + const relationStyles = !item + ? {} + : { + "--relation-col-vs-sm": relation(columns, ssize, "sm"), + "--relation-col-vs-lg": relation(columns, ssize, "lg"), + "--relation-col-vs-xs": relation(columns, ssize, "xs"), + "--relation-col-vs-xl": relation(columns, ssize, "xl"), + "--relation-col-vs-md": relation(columns, ssize, "md"), + }; + + return ( + +
+ {children} +
+
+ ); +} +function relation( + cols: ResponsiveSize, + values: ResponsiveSize, + size: ResponsiveKeys, +) { + const colsNum = typeof cols === "number" ? cols : cols[size] || 12; + return ( + String(Math.round(((values[size] || 1) / colsNum) * 10e7) / 10e5) + "%" + ); } diff --git a/packages/taler-wallet-webextension/src/mui/Paper.tsx b/packages/taler-wallet-webextension/src/mui/Paper.tsx index 52524380b..00eeda324 100644 --- a/packages/taler-wallet-webextension/src/mui/Paper.tsx +++ b/packages/taler-wallet-webextension/src/mui/Paper.tsx @@ -35,7 +35,7 @@ export function Paper({
{children}

; +const defaultVariantMapping = { + h1: "h1", + h2: "h2", + h3: "h3", + h4: "h4", + h5: "h5", + h6: "h6", + subtitle1: "h6", + subtitle2: "h6", + body1: "p", + body2: "p", + inherit: "p", +}; + +const root = css` + margin: 0; +`; + +const noWrapStyle = css` + overflow: "hidden"; + text-overflow: "ellipsis"; + white-space: "nowrap"; +`; +const gutterBottomStyle = css` + margin-bottom: 0.35em; +`; +const paragraphStyle = css` + margin-bottom: 16px; +`; + +export function Typography({ + align, + gutterBottom = false, + noWrap = false, + paragraph = false, + variant = "body1", + children, +}: Props): VNode { + const cmp = paragraph + ? "p" + : defaultVariantMapping[variant as "h1"] || "span"; + + const alignStyle = + align == "inherit" + ? {} + : { + textAlign: align, + }; + return h( + cmp, + { + class: [ + root, + noWrap && noWrapStyle, + gutterBottom && gutterBottomStyle, + paragraph && paragraphStyle, + ].join(" "), + style: { + ...alignStyle, + }, + }, + children, + ); } diff --git a/packages/taler-wallet-webextension/src/mui/style.tsx b/packages/taler-wallet-webextension/src/mui/style.tsx index 84b0538be..e2af05c49 100644 --- a/packages/taler-wallet-webextension/src/mui/style.tsx +++ b/packages/taler-wallet-webextension/src/mui/style.tsx @@ -22,6 +22,14 @@ export function pxToRem(size: number): string { return `${(size / htmlFontSize) * coef}rem`; } +export interface Spacing { + (): string; + (value: number): string; + (topBottom: number, rightLeft: number): string; + (top: number, rightLeft: number, bottom: number): string; + (top: number, right: number, bottom: number, left: number): string; +} + export const theme = createTheme(); export const ripple = css` @@ -117,11 +125,78 @@ function createTheme() { const shadows = createAllShadows(); const transitions = createTransitions({}); const breakpoints = createBreakpoints({}); + const spacing = createSpacing(); const shape = { - borderRadius: css` + roundBorder: css` border-radius: 4px; `, + squareBorder: css` + border-radius: 0px; + `, + circularBorder: css` + border-radius: 50%; + `, }; + + ///////////////////// + ///////////////////// SPACING + ///////////////////// + + function createUnaryUnit(theme: { spacing: number }, defaultValue: number) { + const themeSpacing = theme.spacing || defaultValue; + + if (typeof themeSpacing === "number") { + return (abs: number | string) => { + if (typeof abs === "string") { + return abs; + } + + return themeSpacing * abs; + }; + } + + if (Array.isArray(themeSpacing)) { + return (abs: number | string) => { + if (typeof abs === "string") { + return abs; + } + + return themeSpacing[abs]; + }; + } + + if (typeof themeSpacing === "function") { + return themeSpacing; + } + + return (a: string | number) => ""; + } + + function createUnarySpacing(theme: { spacing: number }) { + return createUnaryUnit(theme, 8); + } + + function createSpacing(spacingInput: number = 8): Spacing { + // Material Design layouts are visually balanced. Most measurements align to an 8dp grid, which aligns both spacing and the overall layout. + // Smaller components, such as icons, can align to a 4dp grid. + // https://material.io/design/layout/understanding-layout.html#usage + const transform = createUnarySpacing({ + spacing: spacingInput, + }); + + const spacing = (...argsInput: ReadonlyArray): string => { + const args = argsInput.length === 0 ? [1] : argsInput; + + return args + .map((argument) => { + const output = transform(argument); + return typeof output === "number" ? `${output}px` : output; + }) + .join(" "); + }; + + return spacing; + } ///////////////////// ///////////////////// BREAKPOINTS ///////////////////// @@ -691,6 +766,7 @@ function createTheme() { shape, transitions, breakpoints, + spacing, pxToRem, }; } -- cgit v1.2.3