Skip to content

Instantly share code, notes, and snippets.

@steida
Created May 9, 2021 18:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save steida/6148aa4b0ec30a044fab18fb2f4f651a to your computer and use it in GitHub Desktop.
Save steida/6148aa4b0ec30a044fab18fb2f4f651a to your computer and use it in GitHub Desktop.
import { ColorSchemeName, Platform, StyleSheet, ViewStyle } from 'react-native';
// AFAIK, Android has a problem with negative margins.
type RhythmSize = 'XXS' | 'XS' | 'Sm' | '' | 'Lg' | 'XL' | 'XXL';
type RhythmProp =
| `${
| 'm'
| 'p'
| `${'m' | 'p'}${'l' | 'r' | 't' | 'b' | 'v' | 'h'}`}${RhythmSize}`
| `${'w' | 'h' | `${'max' | 'min'}${'W' | 'H'}`}${RhythmSize}`
| `${'_' | ''}${'top' | 'bottom' | 'left' | 'right'}${RhythmSize}`;
type RhythmStyles = Record<RhythmProp, ViewStyle>;
// Test with <link rel="stylesheet" href="//basehold.it/27"></link>
const fontSize = 18;
const lineHeightRatio = 1.5;
export const lineHeight = fontSize * lineHeightRatio;
// http://inlehmansterms.net/2014/06/09/groove-to-a-vertical-rhythm
const fontSizeWithLineHeight = (fontSize: number) => {
const lines = Math.ceil(fontSize / lineHeight);
return { fontSize, lineHeight: lines * lineHeight };
};
const colors = {
white: '#fff',
black: '#333',
brand: '#DC2626',
gray: '#9CA3AF',
lightGray: '#E5E7EB',
};
export const createTheme = (colorScheme: ColorSchemeName) =>
StyleSheet.create({
textXS: fontSizeWithLineHeight(14),
textSm: fontSizeWithLineHeight(16),
text: fontSizeWithLineHeight(fontSize),
textLg: fontSizeWithLineHeight(24),
textXL: fontSizeWithLineHeight(32),
textXXL: fontSizeWithLineHeight(42),
textBold: { fontWeight: '500' },
textCenter: { textAlign: 'center' },
textLeft: { textAlign: 'left' },
textRight: { textAlign: 'right' },
textJustify: { textAlign: 'justify' },
mAuto: { margin: 'auto' },
mhAuto: { marginHorizontal: 'auto' },
wFull: { width: '100%' },
hFull: { height: '100%' },
// Note rhythm. Vertical is baseLineHeight, horizontal is baseFontSize.
// RNfW statically renders only actually used styles so we can generate them.
...(() => {
const sizes = [
['XXS', lineHeight / 6, fontSize / 6],
['XS', lineHeight / 4, fontSize / 4],
['Sm', lineHeight / 2, fontSize / 2],
['', lineHeight, fontSize],
['Lg', lineHeight * 2, fontSize * 2],
['XL', lineHeight * 3, fontSize * 3],
['XXL', lineHeight * 4, fontSize * 4],
] as const;
const directions = ['l', 'r', 't', 'b', 'v', 'h'] as const;
const dMap = {
l: 'Left',
r: 'Right',
t: 'Top',
b: 'Bottom',
v: 'Vertical',
h: 'Horizontal',
} as const;
const rhythm: Partial<RhythmStyles> = {};
(['m', 'p'] as const).forEach((a) => {
sizes.forEach(([size, v, h]) => {
const prop = (a + size) as keyof RhythmStyles;
rhythm[prop] =
a === 'm'
? { marginVertical: v, marginHorizontal: h }
: { paddingVertical: v, paddingHorizontal: h };
});
directions.forEach((d) => {
sizes.forEach(([size, v, h]) => {
const prop = (a + d + size) as keyof RhythmStyles;
const mopProp = a === 'm' ? 'margin' : 'padding';
const valueProps = `${mopProp}${dMap[d]}`;
rhythm[prop] = {
[valueProps]: d === 'v' || d === 't' || d === 'b' ? v : h,
};
});
});
});
(['w', 'h'] as const).forEach((a) => {
sizes.forEach(([size, v, h]) => {
const prop = (a + size) as keyof RhythmStyles;
rhythm[prop] = {
// 10 is just an experiment, it should be explicit IMHO.
[a === 'w' ? 'width' : 'height']: (a === 'w' ? h : v) * 10,
};
['max', 'min'].forEach((b) => {
const prop = (b + a.toUpperCase() + size) as keyof RhythmStyles;
rhythm[prop] = {
[b + (a === 'w' ? 'Width' : 'Height')]: (a === 'w' ? h : v) * 10,
};
});
});
});
// `${'_' | ''}${'top' | 'bottom' | 'left' | 'right'}${RhythmSize}`;
['_', ''].forEach((a) => {
['top', 'bottom', 'left', 'right'].forEach((b) => {
sizes.forEach(([size, v, h]) => {
const prop = (a + b + size) as keyof RhythmStyles;
const value = b === 'top' || b === 'bottom' ? v : h;
rhythm[prop] = {
[b]: a === '_' ? -value : value,
};
});
});
});
return rhythm as RhythmStyles;
})(),
color: {
color: colorScheme === 'light' ? colors.black : colors.white,
},
colorInverted: {
color: colorScheme !== 'light' ? colors.black : colors.white,
},
colorBrand: {
color: colors.brand,
},
colorGray: {
color: colors.gray,
},
bgColor: {
backgroundColor: colorScheme === 'light' ? colors.white : colors.black,
},
bgColorInverted: {
backgroundColor: colorScheme !== 'light' ? colors.white : colors.black,
},
bgColorBrand: {
backgroundColor: colors.brand,
},
opacityPressed: {
opacity: 0.75,
},
opacityDisabled: {
opacity: 0.5,
},
rounded: {
borderRadius: 6,
},
overflowHidden: {
overflow: 'hidden',
},
flexRow: {
flexDirection: 'row',
},
flexGrow: {
flex: 1,
},
justifyStart: { justifyContent: 'flex-start' },
justifyEnd: { justifyContent: 'flex-end' },
justifyCenter: { justifyContent: 'center' },
justifyBetween: { justifyContent: 'space-between' },
justifyAround: { justifyContent: 'space-around' },
justifyEvenly: { justifyContent: 'space-evenly' },
itemsStart: { alignItems: 'flex-start' },
itemsEnd: { alignItems: 'flex-end' },
itemsCenter: { alignItems: 'center' },
itemsBaseline: { alignItems: 'baseline' },
itemsStretch: { alignItems: 'stretch' },
absolute: {
position: 'absolute',
},
inset: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
border: {
borderWidth: 1,
},
borderLightGray: {
borderColor: colors.lightGray,
},
borderGray: {
borderColor: colors.gray,
},
borderBlack: {
borderColor: colors.black,
},
_z1: {
zIndex: -1,
},
shadow: {
shadowColor: colors.gray,
shadowOffset: {
width: 0,
height: 6,
},
shadowOpacity: 0.58,
shadowRadius: lineHeight,
},
// @ts-expect-error RNfW
noOutline: Platform.select({
web: {
outlineStyle: 'none',
},
}),
hidden: {
// TODO: Probably opacity 0 for React Native.
// @ts-expect-error RNfW
visibility: 'hidden',
},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment