810 lines
23 KiB
TypeScript
810 lines
23 KiB
TypeScript
import { css } from "@linaria/core";
|
|
import { darken, lighten } from "polished";
|
|
import {
|
|
common,
|
|
purple,
|
|
red,
|
|
orange,
|
|
blue,
|
|
lightBlue,
|
|
green,
|
|
grey,
|
|
// eslint-disable-next-line import/extensions
|
|
} from "./colors/constants";
|
|
// eslint-disable-next-line import/extensions
|
|
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;
|
|
}
|
|
const fontSize = 14;
|
|
const htmlFontSize = 16;
|
|
const coef = fontSize / 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`
|
|
background-position: center;
|
|
|
|
transition: background 0.5s;
|
|
&:hover {
|
|
background: #47a7f5 radial-gradient(circle, transparent 1%, #47a7f5 1%)
|
|
center/15000%;
|
|
}
|
|
&:active {
|
|
background-color: #6eb9f7;
|
|
background-size: 100%;
|
|
transition: background 0s;
|
|
}
|
|
`;
|
|
|
|
function createTheme() {
|
|
const light = {
|
|
// The colors used to style the text.
|
|
text: {
|
|
// The most important text.
|
|
primary: "rgba(0, 0, 0, 0.87)",
|
|
// Secondary text.
|
|
secondary: "rgba(0, 0, 0, 0.6)",
|
|
// Disabled text have even lower visual prominence.
|
|
disabled: "rgba(0, 0, 0, 0.38)",
|
|
},
|
|
// The color used to divide different elements.
|
|
divider: "rgba(0, 0, 0, 0.12)",
|
|
// The background colors used to style the surfaces.
|
|
// Consistency between these values is important.
|
|
background: {
|
|
paper: common.white,
|
|
default: common.white,
|
|
},
|
|
// The colors used to style the action elements.
|
|
action: {
|
|
// The color of an active action like an icon button.
|
|
active: "rgba(0, 0, 0, 0.54)",
|
|
// The color of an hovered action.
|
|
hover: "rgba(0, 0, 0, 0.04)",
|
|
hoverOpacity: 0.04,
|
|
// The color of a selected action.
|
|
selected: "rgba(0, 0, 0, 0.08)",
|
|
selectedOpacity: 0.08,
|
|
// The color of a disabled action.
|
|
disabled: "rgba(0, 0, 0, 0.26)",
|
|
// The background color of a disabled action.
|
|
disabledBackground: "rgba(0, 0, 0, 0.12)",
|
|
disabledOpacity: 0.38,
|
|
focus: "rgba(0, 0, 0, 0.12)",
|
|
focusOpacity: 0.12,
|
|
activatedOpacity: 0.12,
|
|
},
|
|
};
|
|
|
|
const dark = {
|
|
text: {
|
|
primary: common.white,
|
|
secondary: "rgba(255, 255, 255, 0.7)",
|
|
disabled: "rgba(255, 255, 255, 0.5)",
|
|
icon: "rgba(255, 255, 255, 0.5)",
|
|
},
|
|
divider: "rgba(255, 255, 255, 0.12)",
|
|
background: {
|
|
paper: "#121212",
|
|
default: "#121212",
|
|
},
|
|
action: {
|
|
active: common.white,
|
|
hover: "rgba(255, 255, 255, 0.08)",
|
|
hoverOpacity: 0.08,
|
|
selected: "rgba(255, 255, 255, 0.16)",
|
|
selectedOpacity: 0.16,
|
|
disabled: "rgba(255, 255, 255, 0.3)",
|
|
disabledBackground: "rgba(255, 255, 255, 0.12)",
|
|
disabledOpacity: 0.38,
|
|
focus: "rgba(255, 255, 255, 0.12)",
|
|
focusOpacity: 0.12,
|
|
activatedOpacity: 0.24,
|
|
},
|
|
};
|
|
|
|
const defaultFontFamily = '"Roboto", "Helvetica", "Arial", sans-serif';
|
|
|
|
const shadowKeyUmbraOpacity = 0.2;
|
|
const shadowKeyPenumbraOpacity = 0.14;
|
|
const shadowAmbientShadowOpacity = 0.12;
|
|
|
|
const typography = createTypography({});
|
|
const palette = createPalette({});
|
|
const shadows = createAllShadows();
|
|
const transitions = createTransitions({});
|
|
const breakpoints = createBreakpoints({});
|
|
const spacing = createSpacing();
|
|
const shape = {
|
|
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 = 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 | undefined>
|
|
): string => {
|
|
const args = argsInput.length === 0 ? [1] : argsInput;
|
|
|
|
return args
|
|
.map((argument) => {
|
|
if (argument === undefined) return "";
|
|
const output = transform(argument);
|
|
return typeof output === "number" ? `${output}px` : output;
|
|
})
|
|
.join(" ");
|
|
};
|
|
|
|
return spacing;
|
|
}
|
|
/////////////////////
|
|
///////////////////// BREAKPOINTS
|
|
/////////////////////
|
|
function createBreakpoints(breakpoints: any) {
|
|
const {
|
|
// The breakpoint **start** at this value.
|
|
// For instance with the first breakpoint xs: [xs, sm).
|
|
values = {
|
|
xs: 0,
|
|
sm: 600,
|
|
md: 900,
|
|
lg: 1200,
|
|
xl: 1536, // large screen
|
|
},
|
|
unit = "px",
|
|
step = 5,
|
|
// ...other
|
|
} = breakpoints;
|
|
|
|
const keys = Object.keys(values);
|
|
|
|
function up(key: any) {
|
|
const value = typeof values[key] === "number" ? values[key] : key;
|
|
return `@media (min-width:${value}${unit})`;
|
|
}
|
|
|
|
function down(key: any) {
|
|
const value = typeof values[key] === "number" ? values[key] : key;
|
|
return `@media (max-width:${value - step / 100}${unit})`;
|
|
}
|
|
|
|
function between(start: any, end: any) {
|
|
const endIndex = keys.indexOf(end);
|
|
|
|
return (
|
|
`@media (min-width:${
|
|
typeof values[start] === "number" ? values[start] : start
|
|
}${unit}) and ` +
|
|
`(max-width:${
|
|
(endIndex !== -1 && typeof values[keys[endIndex]] === "number"
|
|
? values[keys[endIndex]]
|
|
: end) -
|
|
step / 100
|
|
}${unit})`
|
|
);
|
|
}
|
|
|
|
function only(key: any) {
|
|
if (keys.indexOf(key) + 1 < keys.length) {
|
|
return between(key, keys[keys.indexOf(key) + 1]);
|
|
}
|
|
|
|
return up(key);
|
|
}
|
|
|
|
function not(key: any) {
|
|
// handle first and last key separately, for better readability
|
|
const keyIndex = keys.indexOf(key);
|
|
if (keyIndex === 0) {
|
|
return up(keys[1]);
|
|
}
|
|
if (keyIndex === keys.length - 1) {
|
|
return down(keys[keyIndex]);
|
|
}
|
|
|
|
return between(key, keys[keys.indexOf(key) + 1]).replace(
|
|
"@media",
|
|
"@media not all and",
|
|
);
|
|
}
|
|
|
|
return {
|
|
keys,
|
|
values,
|
|
up,
|
|
down,
|
|
between,
|
|
only,
|
|
not,
|
|
unit,
|
|
// ...other,
|
|
};
|
|
}
|
|
|
|
/////////////////////
|
|
///////////////////// SHADOWS
|
|
/////////////////////
|
|
function createShadow(...px: number[]): string {
|
|
return [
|
|
`${px[0]}px ${px[1]}px ${px[2]}px ${px[3]}px rgba(0,0,0,${shadowKeyUmbraOpacity})`,
|
|
`${px[4]}px ${px[5]}px ${px[6]}px ${px[7]}px rgba(0,0,0,${shadowKeyPenumbraOpacity})`,
|
|
`${px[8]}px ${px[9]}px ${px[10]}px ${px[11]}px rgba(0,0,0,${shadowAmbientShadowOpacity})`,
|
|
].join(",");
|
|
}
|
|
|
|
function createAllShadows() {
|
|
// Values from https://github.com/material-components/material-components-web/blob/be8747f94574669cb5e7add1a7c54fa41a89cec7/packages/mdc-elevation/_variables.scss
|
|
return [
|
|
"none",
|
|
createShadow(0, 2, 1, -1, 0, 1, 1, 0, 0, 1, 3, 0),
|
|
createShadow(0, 3, 1, -2, 0, 2, 2, 0, 0, 1, 5, 0),
|
|
createShadow(0, 3, 3, -2, 0, 3, 4, 0, 0, 1, 8, 0),
|
|
createShadow(0, 2, 4, -1, 0, 4, 5, 0, 0, 1, 10, 0),
|
|
createShadow(0, 3, 5, -1, 0, 5, 8, 0, 0, 1, 14, 0),
|
|
createShadow(0, 3, 5, -1, 0, 6, 10, 0, 0, 1, 18, 0),
|
|
createShadow(0, 4, 5, -2, 0, 7, 10, 1, 0, 2, 16, 1),
|
|
createShadow(0, 5, 5, -3, 0, 8, 10, 1, 0, 3, 14, 2),
|
|
createShadow(0, 5, 6, -3, 0, 9, 12, 1, 0, 3, 16, 2),
|
|
createShadow(0, 6, 6, -3, 0, 10, 14, 1, 0, 4, 18, 3),
|
|
createShadow(0, 6, 7, -4, 0, 11, 15, 1, 0, 4, 20, 3),
|
|
createShadow(0, 7, 8, -4, 0, 12, 17, 2, 0, 5, 22, 4),
|
|
createShadow(0, 7, 8, -4, 0, 13, 19, 2, 0, 5, 24, 4),
|
|
createShadow(0, 7, 9, -4, 0, 14, 21, 2, 0, 5, 26, 4),
|
|
createShadow(0, 8, 9, -5, 0, 15, 22, 2, 0, 6, 28, 5),
|
|
createShadow(0, 8, 10, -5, 0, 16, 24, 2, 0, 6, 30, 5),
|
|
createShadow(0, 8, 11, -5, 0, 17, 26, 2, 0, 6, 32, 5),
|
|
createShadow(0, 9, 11, -5, 0, 18, 28, 2, 0, 7, 34, 6),
|
|
createShadow(0, 9, 12, -6, 0, 19, 29, 2, 0, 7, 36, 6),
|
|
createShadow(0, 10, 13, -6, 0, 20, 31, 3, 0, 8, 38, 7),
|
|
createShadow(0, 10, 13, -6, 0, 21, 33, 3, 0, 8, 40, 7),
|
|
createShadow(0, 10, 14, -6, 0, 22, 35, 3, 0, 8, 42, 7),
|
|
createShadow(0, 11, 14, -7, 0, 23, 36, 3, 0, 9, 44, 8),
|
|
createShadow(0, 11, 15, -7, 0, 24, 38, 3, 0, 9, 46, 8),
|
|
];
|
|
}
|
|
|
|
/////////////////////
|
|
///////////////////// TYPOGRAPHY
|
|
/////////////////////
|
|
/**
|
|
* @see @link{https://material.io/design/typography/the-type-system.html}
|
|
* @see @link{https://material.io/design/typography/understanding-typography.html}
|
|
*/
|
|
function createTypography(typography: any) {
|
|
// const {
|
|
const fontFamily = defaultFontFamily,
|
|
// The default font size of the Material Specification.
|
|
fontSize = 14, // px
|
|
fontWeightLight = 300,
|
|
fontWeightRegular = 400,
|
|
fontWeightMedium = 500,
|
|
fontWeightBold = 700,
|
|
// Tell MUI what's the font-size on the html element.
|
|
// 16px is the default font-size used by browsers.
|
|
htmlFontSize = 16;
|
|
// Apply the CSS properties to all the variants.
|
|
// allVariants,
|
|
// pxToRem: pxToRem2,
|
|
// ...other
|
|
// } = typography;
|
|
const variants = {
|
|
// (fontWeight, size, lineHeight, letterSpacing, casing) =>
|
|
// h1: buildVariant(fontWeightLight, 96, 1.167, -1.5),
|
|
// h2: buildVariant(fontWeightLight, 60, 1.2, -0.5),
|
|
// h3: buildVariant(fontWeightRegular, 48, 1.167, 0),
|
|
// h4: buildVariant(fontWeightRegular, 34, 1.235, 0.25),
|
|
// h5: buildVariant(fontWeightRegular, 24, 1.334, 0),
|
|
// h6: buildVariant(fontWeightMedium, 20, 1.6, 0.15),
|
|
// subtitle1: buildVariant(fontWeightRegular, 16, 1.75, 0.15),
|
|
// subtitle2: buildVariant(fontWeightMedium, 14, 1.57, 0.1),
|
|
body1: css`
|
|
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
|
font-weight: ${fontWeightRegular};
|
|
font-size: ${pxToRem(16)};
|
|
line-height: 1.5;
|
|
letter-spacing: ${round(0.15 / 16)}em;
|
|
`,
|
|
// body1: buildVariant(fontWeightRegular, 16, 1.5, 0.15),
|
|
body2: css`
|
|
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
|
font-weight: ${fontWeightRegular};
|
|
font-size: ${pxToRem(14)};
|
|
line-height: 1.43;
|
|
letter-spacing: ${round(0.15 / 14)}em;
|
|
`,
|
|
// body2: buildVariant(fontWeightRegular, 14, 1.43, 0.15),
|
|
button: css`
|
|
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
|
|
font-weight: ${fontWeightMedium};
|
|
font-size: ${pxToRem(14)};
|
|
line-height: 1.75;
|
|
letter-spacing: ${round(0.4 / 14)}em;
|
|
text-transform: uppercase;
|
|
`,
|
|
/* 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),
|
|
};
|
|
|
|
return deepmerge(
|
|
{
|
|
htmlFontSize,
|
|
pxToRem,
|
|
fontFamily,
|
|
fontSize,
|
|
fontWeightLight,
|
|
fontWeightRegular,
|
|
fontWeightMedium,
|
|
fontWeightBold,
|
|
...variants,
|
|
},
|
|
// other,
|
|
{
|
|
clone: false, // No need to clone deep
|
|
},
|
|
);
|
|
}
|
|
|
|
/////////////////////
|
|
///////////////////// MIXINS
|
|
/////////////////////
|
|
function createMixins(breakpoints: any, spacing: any, mixins: any) {
|
|
return {
|
|
toolbar: {
|
|
minHeight: 56,
|
|
[`${breakpoints.up("xs")} and (orientation: landscape)`]: {
|
|
minHeight: 48,
|
|
},
|
|
[breakpoints.up("sm")]: {
|
|
minHeight: 64,
|
|
},
|
|
},
|
|
...mixins,
|
|
};
|
|
}
|
|
|
|
/////////////////////
|
|
///////////////////// TRANSITION
|
|
/////////////////////
|
|
function formatMs(milliseconds: number) {
|
|
return `${Math.round(milliseconds)}ms`;
|
|
}
|
|
|
|
function getAutoHeightDuration(height: number) {
|
|
if (!height) {
|
|
return 0;
|
|
}
|
|
|
|
const constant = height / 36;
|
|
|
|
// https://www.wolframalpha.com/input/?i=(4+%2B+15+*+(x+%2F+36+)+**+0.25+%2B+(x+%2F+36)+%2F+5)+*+10
|
|
return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
|
|
}
|
|
|
|
function createTransitions(inputTransitions: any) {
|
|
// Follow https://material.google.com/motion/duration-easing.html#duration-easing-natural-easing-curves
|
|
// to learn the context in which each easing should be used.
|
|
const easing = {
|
|
// This is the most common easing curve.
|
|
easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
// Objects enter the screen at full velocity from off-screen and
|
|
// slowly decelerate to a resting point.
|
|
easeOut: "cubic-bezier(0.0, 0, 0.2, 1)",
|
|
// Objects leave the screen at full velocity. They do not decelerate when off-screen.
|
|
easeIn: "cubic-bezier(0.4, 0, 1, 1)",
|
|
// The sharp curve is used by objects that may return to the screen at any time.
|
|
sharp: "cubic-bezier(0.4, 0, 0.6, 1)",
|
|
};
|
|
|
|
// Follow https://material.io/guidelines/motion/duration-easing.html#duration-easing-common-durations
|
|
// to learn when use what timing
|
|
const duration = {
|
|
shortest: 150,
|
|
shorter: 200,
|
|
short: 250,
|
|
// most basic recommended timing
|
|
standard: 300,
|
|
// this is to be used in complex animations
|
|
complex: 375,
|
|
// recommended when something is entering screen
|
|
enteringScreen: 225,
|
|
// recommended when something is leaving screen
|
|
leavingScreen: 195,
|
|
};
|
|
|
|
const mergedEasing = {
|
|
...easing,
|
|
...inputTransitions.easing,
|
|
};
|
|
|
|
const mergedDuration = {
|
|
...duration,
|
|
...inputTransitions.duration,
|
|
};
|
|
|
|
const create = (props = ["all"], options = {} as any) => {
|
|
const {
|
|
duration: durationOption = mergedDuration.standard,
|
|
easing: easingOption = mergedEasing.easeInOut,
|
|
delay = 0,
|
|
// ...other
|
|
} = options;
|
|
|
|
return (Array.isArray(props) ? props : [props])
|
|
.map(
|
|
(animatedProp) =>
|
|
`${animatedProp} ${
|
|
typeof durationOption === "string"
|
|
? durationOption
|
|
: formatMs(durationOption)
|
|
} ${easingOption} ${
|
|
typeof delay === "string" ? delay : formatMs(delay)
|
|
}`,
|
|
)
|
|
.join(",");
|
|
};
|
|
|
|
return {
|
|
getAutoHeightDuration,
|
|
create,
|
|
...inputTransitions,
|
|
easing: mergedEasing,
|
|
duration: mergedDuration,
|
|
};
|
|
}
|
|
|
|
/////////////////////
|
|
///////////////////// PALETTE
|
|
/////////////////////
|
|
function createPalette(palette: any) {
|
|
// const {
|
|
const mode: "light" | "dark" = "light";
|
|
const contrastThreshold = 3;
|
|
const tonalOffset = 0.2;
|
|
// ...other
|
|
// } = palette;
|
|
|
|
const primary = palette.primary || getDefaultPrimary(mode);
|
|
const secondary = palette.secondary || getDefaultSecondary(mode);
|
|
const error = palette.error || getDefaultError(mode);
|
|
const info = palette.info || getDefaultInfo(mode);
|
|
const success = palette.success || getDefaultSuccess(mode);
|
|
const warning = palette.warning || getDefaultWarning(mode);
|
|
|
|
// Use the same logic as
|
|
// Bootstrap: https://github.com/twbs/bootstrap/blob/1d6e3710dd447de1a200f29e8fa521f8a0908f70/scss/_functions.scss#L59
|
|
// and material-components-web https://github.com/material-components/material-components-web/blob/ac46b8863c4dab9fc22c4c662dc6bd1b65dd652f/packages/mdc-theme/_functions.scss#L54
|
|
function getContrastText(background: string): string {
|
|
const contrastText =
|
|
getContrastRatio(background, dark.text.primary) >= contrastThreshold
|
|
? dark.text.primary
|
|
: light.text.primary;
|
|
|
|
return contrastText;
|
|
}
|
|
|
|
const augmentColor = ({
|
|
color,
|
|
name,
|
|
mainShade = 500,
|
|
lightShade = 300,
|
|
darkShade = 700,
|
|
}: any) => {
|
|
color = { ...color };
|
|
if (!color.main && color[mainShade]) {
|
|
color.main = color[mainShade];
|
|
}
|
|
|
|
addLightOrDark(color, "light", lightShade, tonalOffset);
|
|
addLightOrDark(color, "dark", darkShade, tonalOffset);
|
|
if (!color.contrastText) {
|
|
color.contrastText = getContrastText(color.main);
|
|
}
|
|
|
|
return color;
|
|
};
|
|
|
|
const modes = { dark, light };
|
|
|
|
// if (process.env.NODE_ENV !== "production") {
|
|
// if (!modes[mode]) {
|
|
// console.error(`MUI: The palette mode \`${mode}\` is not supported.`);
|
|
// }
|
|
// }
|
|
const paletteOutput = deepmerge(
|
|
{
|
|
// A collection of common colors.
|
|
common,
|
|
// The palette mode, can be light or dark.
|
|
mode,
|
|
// The colors used to represent primary interface elements for a user.
|
|
primary: augmentColor({ color: primary, name: "primary" }),
|
|
// The colors used to represent secondary interface elements for a user.
|
|
secondary: augmentColor({
|
|
color: secondary,
|
|
name: "secondary",
|
|
mainShade: "A400",
|
|
lightShade: "A200",
|
|
darkShade: "A700",
|
|
}),
|
|
// The colors used to represent interface elements that the user should be made aware of.
|
|
error: augmentColor({ color: error, name: "error" }),
|
|
// The colors used to represent potentially dangerous actions or important messages.
|
|
warning: augmentColor({ color: warning, name: "warning" }),
|
|
// The colors used to present information to the user that is neutral and not necessarily important.
|
|
info: augmentColor({ color: info, name: "info" }),
|
|
// The colors used to indicate the successful completion of an action that user triggered.
|
|
success: augmentColor({ color: success, name: "success" }),
|
|
// The grey colors.
|
|
grey,
|
|
// Used by `getContrastText()` to maximize the contrast between
|
|
// the background and the text.
|
|
contrastThreshold,
|
|
// Takes a background color and returns the text color that maximizes the contrast.
|
|
getContrastText,
|
|
// Generate a rich color object.
|
|
augmentColor,
|
|
// Used by the functions below to shift a color's luminance by approximately
|
|
// two indexes within its tonal palette.
|
|
// E.g., shift from Red 500 to Red 300 or Red 700.
|
|
tonalOffset,
|
|
// The light and dark mode object.
|
|
...modes[mode],
|
|
},
|
|
// other:
|
|
{},
|
|
);
|
|
|
|
return paletteOutput;
|
|
}
|
|
|
|
function addLightOrDark(
|
|
intent: any,
|
|
direction: any,
|
|
shade: any,
|
|
tonalOffset: any,
|
|
): void {
|
|
const tonalOffsetLight = tonalOffset.light || tonalOffset;
|
|
const tonalOffsetDark = tonalOffset.dark || tonalOffset * 1.5;
|
|
|
|
if (!intent[direction]) {
|
|
if (intent.hasOwnProperty(shade)) {
|
|
intent[direction] = intent[shade];
|
|
} else if (direction === "light") {
|
|
intent.light = lighten(intent.main, tonalOffsetLight);
|
|
} else if (direction === "dark") {
|
|
intent.dark = darken(intent.main, tonalOffsetDark);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getDefaultPrimary(mode = "light") {
|
|
if (mode === "dark") {
|
|
return {
|
|
main: blue[200],
|
|
light: blue[50],
|
|
dark: blue[400],
|
|
};
|
|
}
|
|
return {
|
|
main: blue[700],
|
|
light: blue[400],
|
|
dark: blue[800],
|
|
};
|
|
}
|
|
|
|
function getDefaultSecondary(mode = "light") {
|
|
if (mode === "dark") {
|
|
return {
|
|
main: purple[200],
|
|
light: purple[50],
|
|
dark: purple[400],
|
|
};
|
|
}
|
|
return {
|
|
main: purple[500],
|
|
light: purple[300],
|
|
dark: purple[700],
|
|
};
|
|
}
|
|
|
|
function getDefaultError(mode = "light") {
|
|
if (mode === "dark") {
|
|
return {
|
|
main: red[500],
|
|
light: red[300],
|
|
dark: red[700],
|
|
};
|
|
}
|
|
return {
|
|
main: red[700],
|
|
light: red[400],
|
|
dark: red[800],
|
|
};
|
|
}
|
|
|
|
function getDefaultInfo(mode = "light") {
|
|
if (mode === "dark") {
|
|
return {
|
|
main: lightBlue[400],
|
|
light: lightBlue[300],
|
|
dark: lightBlue[700],
|
|
};
|
|
}
|
|
return {
|
|
main: lightBlue[700],
|
|
light: lightBlue[500],
|
|
dark: lightBlue[900],
|
|
};
|
|
}
|
|
|
|
function getDefaultSuccess(mode = "light") {
|
|
if (mode === "dark") {
|
|
return {
|
|
main: green[400],
|
|
light: green[300],
|
|
dark: green[700],
|
|
};
|
|
}
|
|
return {
|
|
main: green[800],
|
|
light: green[500],
|
|
dark: green[900],
|
|
};
|
|
}
|
|
|
|
function getDefaultWarning(mode = "light") {
|
|
if (mode === "dark") {
|
|
return {
|
|
main: orange[400],
|
|
light: orange[300],
|
|
dark: orange[700],
|
|
};
|
|
}
|
|
return {
|
|
main: "#ed6c02",
|
|
light: orange[500],
|
|
dark: orange[900],
|
|
};
|
|
}
|
|
|
|
/////////////////////
|
|
///////////////////// DEEP MERGE
|
|
/////////////////////
|
|
function isPlainObject(item: unknown): item is Record<keyof any, unknown> {
|
|
return (
|
|
item !== null && typeof item === "object" && item.constructor === Object
|
|
);
|
|
}
|
|
|
|
interface DeepmergeOptions {
|
|
clone?: boolean;
|
|
}
|
|
|
|
function deepmerge<T>(
|
|
target: T,
|
|
source: unknown,
|
|
options: DeepmergeOptions = { clone: true },
|
|
): T {
|
|
const output = options.clone ? { ...target } : target;
|
|
|
|
if (isPlainObject(target) && isPlainObject(source)) {
|
|
Object.keys(source).forEach((key) => {
|
|
// Avoid prototype pollution
|
|
if (key === "__proto__") {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
isPlainObject(source[key]) &&
|
|
key in target &&
|
|
isPlainObject(target[key])
|
|
) {
|
|
// Since `output` is a clone of `target` and we have narrowed `target` in this block we can cast to the same type.
|
|
(output as Record<keyof any, unknown>)[key] = deepmerge(
|
|
target[key],
|
|
source[key],
|
|
options,
|
|
);
|
|
} else {
|
|
(output as Record<keyof any, unknown>)[key] = source[key];
|
|
}
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
return {
|
|
typography,
|
|
palette,
|
|
shadows,
|
|
shape,
|
|
transitions,
|
|
breakpoints,
|
|
spacing,
|
|
pxToRem,
|
|
};
|
|
}
|