grid implementation

This commit is contained in:
Sebastian 2022-03-10 23:13:10 -03:00
parent 60a50babd1
commit 2150f3d96b
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
10 changed files with 744 additions and 28 deletions

View File

@ -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 <http://www.gnu.org/licenses/>
*/
/**
*
* @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 (
<div
style={{
display: "flex",
backgroundColor: "lightgray",
padding: 10,
width: "100%",
// width: 400,
// height: 400,
justifyContent: "center",
}}
>
<div style={{ flexGrow: 1 }}>{children}</div>
</div>
);
}
export const BasicExample = () => (
<Fragment>
<Wrapper>
<Banner />
</Wrapper>
</Fragment>
);

View File

@ -1,33 +1,58 @@
import { h, Fragment, VNode } from "preact"; import { h, Fragment, VNode } from "preact";
import { Divider } from "../mui/Divider"; import { Divider } from "../mui/Divider";
import { Button } from "./styled/index.js"; import { Button } from "../mui/Button";
import { Typography } from "../mui/Typography"; import { Typography } from "../mui/Typography";
import { Avatar } from "../mui/Avatar"; import { Avatar } from "../mui/Avatar";
import { Grid } from "../mui/Grid"; import { Grid } from "../mui/Grid";
import { Paper } from "../mui/Paper"; 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 { function SignalWifiOffIcon({ ...rest }: any): VNode {
return <Fragment />; return <Icon {...rest} />;
} }
function Banner({}: {}) { export function Banner({}: {}) {
return ( return (
<Fragment> <Fragment>
<Paper elevation={0} /*className={classes.paper}*/> <Paper elevation={3} /*className={classes.paper}*/>
<Grid container wrap="nowrap" spacing={16} alignItems="center"> <Grid
<Grid item> container
<Avatar /*className={classes.avatar}*/> // wrap="nowrap"
<SignalWifiOffIcon /> // spacing={10}
alignItems="center"
columns={3}
>
<Grid item xs={1}>
<Avatar>
<SignalWifiOffIcon style={{ backgroundColor: "red" }} />
</Avatar> </Avatar>
</Grid> </Grid>
<Grid item> <Grid item xs={1}>
<Typography> <Typography>
You have lost connection to the internet. This app is offline. You have lost connection to the internet. This app is offline.
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
<Grid container justify="flex-end" spacing={8}> <Grid container justifyContent="flex-end" spacing={8} columns={3}>
<Grid item> <Grid item xs={1}>
<Button color="primary">Turn on wifi</Button> <Button color="primary">Turn on wifi</Button>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -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")`; 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` export const NiceSelect = styled.div`

View File

@ -1,5 +1,53 @@
import { css } from "@linaria/core";
import { h, Fragment, VNode, ComponentChildren } from "preact"; import { h, Fragment, VNode, ComponentChildren } from "preact";
import { theme } from "./style";
export function Avatar({}: { children: ComponentChildren }): VNode { const root = css`
return <Fragment />; 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 (
<div class={[root, borderStyle].join(" ")} {...rest}>
{children}
</div>
);
} }

View File

@ -185,7 +185,7 @@ export function Button({
disabled={disabled} disabled={disabled}
class={[ class={[
theme.typography.button, theme.typography.button,
theme.shape.borderRadius, theme.shape.roundBorder,
ripple, ripple,
baseStyle, baseStyle,
button, button,

View File

@ -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 <http://www.gnu.org/licenses/>
*/
/**
*
* @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 (
<div
style={{
padding: 10,
backgroundColor: "white",
textAlign: "center",
color: "back",
}}
>
{children}
</div>
);
}
function Wrapper({ children }: any) {
return (
<div
style={{
display: "flex",
backgroundColor: "lightgray",
padding: 10,
width: "100%",
// width: 400,
// height: 400,
justifyContent: "center",
}}
>
<div style={{ flexGrow: 1 }}>{children}</div>
</div>
);
}
export const BasicExample = () => (
<Fragment>
<Wrapper>
<Grid container spacing={2}>
<Grid item xs={8}>
<Item>xs=8</Item>
</Grid>
<Grid item xs={4}>
<Item>xs=4</Item>
</Grid>
<Grid item xs={4}>
<Item>xs=4</Item>
</Grid>
<Grid item xs={8}>
<Item>xs=8</Item>
</Grid>
</Grid>
</Wrapper>
<Wrapper>
<Grid container spacing={2}>
<Grid item xs={6} md={8}>
<Item>xs=6 md=8</Item>
</Grid>
<Grid item xs={6} md={4}>
<Item>xs=6 md=4</Item>
</Grid>
<Grid item xs={6} md={4}>
<Item>xs=6 md=4</Item>
</Grid>
<Grid item xs={6} md={8}>
<Item>xs=6 md=8</Item>
</Grid>
</Grid>
</Wrapper>
</Fragment>
);
export const Responsive12ColumnsSize = () => (
<Fragment>
<Wrapper>
<p>Item size is responsive: xs=6 sm=4 md=2</p>
<Grid container spacing={1} columns={12}>
{Array.from(Array(6)).map((_, index) => (
<Grid item xs={6} sm={4} md={2} key={index}>
<Item>item {index}</Item>
</Grid>
))}
</Grid>
</Wrapper>
<Wrapper>
<p>Item size is fixed</p>
<Grid container spacing={1} columns={12}>
{Array.from(Array(6)).map((_, index) => (
<Grid item xs={6} key={index}>
<Item>item {index}</Item>
</Grid>
))}
</Grid>
</Wrapper>
</Fragment>
);
export const Responsive12Spacing = () => (
<Fragment>
<Wrapper>
<p>Item space is responsive: xs=1 sm=2 md=3</p>
<Grid container spacing={{ xs: 2, sm: 4, md: 6 }} columns={12}>
{Array.from(Array(6)).map((_, index) => (
<Grid item xs={6} key={index}>
<Item>item {index}</Item>
</Grid>
))}
</Grid>
</Wrapper>
<Wrapper>
<p>Item space is fixed</p>
<Grid container spacing={1} columns={12}>
{Array.from(Array(6)).map((_, index) => (
<Grid item xs={6} key={index}>
<Item>item {index}</Item>
</Grid>
))}
</Grid>
</Wrapper>
<Wrapper>
<p>Item row space is responsive: xs=6 sm=4 md=1</p>
<Grid
container
rowSpacing={{ xs: 6, sm: 3, md: 1 }}
columnSpacing={1}
columns={12}
>
{Array.from(Array(6)).map((_, index) => (
<Grid item xs={6} key={index}>
<Item>item {index}</Item>
</Grid>
))}
</Grid>
</Wrapper>
<Wrapper>
<p>Item col space is responsive: xs=6 sm=3 md=1</p>
<Grid
container
columnSpacing={{ xs: 6, sm: 3, md: 1 }}
rowSpacing={1}
columns={12}
>
{Array.from(Array(6)).map((_, index) => (
<Grid item xs={6} key={index}>
<Item>item {index}</Item>
</Grid>
))}
</Grid>
</Wrapper>
</Fragment>
);
export const Example = () => (
<Wrapper>
<p>Item row space is responsive: xs=6 sm=4 md=1</p>
<Grid container rowSpacing={3} columnSpacing={1} columns={12}>
{Array.from(Array(6)).map((_, index) => (
<Grid item xs={6} key={index}>
<Item>item {index}</Item>
</Grid>
))}
</Grid>
</Wrapper>
);

View File

@ -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<ResponsiveSize>;
container?: boolean; container?: boolean;
wrap?: string;
item?: boolean; item?: boolean;
spacing?: number;
alignItems?: string; direction?: "column-reverse" | "column" | "row-reverse" | "row";
justify?: string;
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<ResponsiveSize>;
columnSpacing?: SpacingSizes | Partial<ResponsiveSize>;
rowSpacing?: SpacingSizes | Partial<ResponsiveSize>;
alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | "baseline";
justifyContent?:
| "flex-start"
| "flex-end"
| "center"
| "space-around"
| "space-between"
| "space-evenly";
zeroMinWidth?: boolean;
children: ComponentChildren; children: ComponentChildren;
}): VNode { }
return <Fragment />; 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<ResponsiveSize>(toResponsive(12));
function toResponsive(v: number | Partial<ResponsiveSize>): 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 (
<GridContext.Provider value={columns}>
<div
id={container ? "container" : "item"}
class={[
root,
container && containerStyle,
item && itemStyle,
zeroMinWidth && zeroMinWidthStyle,
sizeVariant,
container && columnGapVariant,
container && rowGapVariant,
].join(" ")}
style={{
...relationStyles,
...spacingStyles,
justifyContent,
alignItems,
}}
>
{children}
</div>
</GridContext.Provider>
);
}
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) + "%"
);
} }

View File

@ -35,7 +35,7 @@ export function Paper({
<div <div
class={[ class={[
baseStyle, baseStyle,
!square && theme.shape.borderRadius, !square && theme.shape.roundBorder,
borderVariant[variant], borderVariant[variant],
].join(" ")} ].join(" ")}
style={{ style={{

View File

@ -1,9 +1,92 @@
import { css } from "@linaria/core";
import { h, Fragment, VNode, ComponentChildren } from "preact"; import { h, Fragment, VNode, ComponentChildren } from "preact";
type VariantEnum =
| "body1"
| "body2"
| "button"
| "caption"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "inherit"
| "overline"
| "subtitle1"
| "subtitle2";
interface Props { interface Props {
children: ComponentChildren; align?: "center" | "inherit" | "justify" | "left" | "right";
gutterBottom?: boolean;
noWrap?: boolean;
paragraph?: boolean;
variant?: VariantEnum;
children?: ComponentChildren;
} }
export function Typography({ children }: Props): VNode { const defaultVariantMapping = {
return <p>{children}</p>; 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,
);
} }

View File

@ -22,6 +22,14 @@ export function pxToRem(size: number): string {
return `${(size / htmlFontSize) * coef}rem`; 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 theme = createTheme();
export const ripple = css` export const ripple = css`
@ -117,11 +125,78 @@ function createTheme() {
const shadows = createAllShadows(); const shadows = createAllShadows();
const transitions = createTransitions({}); const transitions = createTransitions({});
const breakpoints = createBreakpoints({}); const breakpoints = createBreakpoints({});
const spacing = createSpacing();
const shape = { const shape = {
borderRadius: css` roundBorder: css`
border-radius: 4px; 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<number | string>): 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 ///////////////////// BREAKPOINTS
///////////////////// /////////////////////
@ -691,6 +766,7 @@ function createTheme() {
shape, shape,
transitions, transitions,
breakpoints, breakpoints,
spacing,
pxToRem, pxToRem,
}; };
} }