feat: more advanced theme engine

This commit is contained in:
Roman Hotsiy 2018-03-16 17:02:31 +02:00
parent ea3c731464
commit 1df690a964
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
15 changed files with 154 additions and 85 deletions

View File

@ -99,6 +99,7 @@
"mobx-react": "^4.3.3", "mobx-react": "^4.3.3",
"openapi-sampler": "1.0.0-beta.9", "openapi-sampler": "1.0.0-beta.9",
"perfect-scrollbar": "^1.3.0", "perfect-scrollbar": "^1.3.0",
"polished": "^1.9.2",
"prismjs": "^1.8.1", "prismjs": "^1.8.1",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react-dropdown": "^1.3.0", "react-dropdown": "^1.3.0",

View File

@ -1,12 +1,12 @@
import styled from '../styled-components'; import styled from '../styled-components';
import { transparentizeHex } from '../utils/styled'; import { transparentize } from 'polished';
import { deprecatedCss } from './mixins'; import { deprecatedCss } from './mixins';
export const PropertiesTableCaption = styled.caption` export const PropertiesTableCaption = styled.caption`
text-align: right; text-align: right;
font-size: 0.9em; font-size: 0.9em;
font-weight: normal; font-weight: normal;
color: ${props => transparentizeHex(props.theme.colors.text, 0.4)}; color: ${props => transparentize(0.4, props.theme.colors.text)};
`; `;
export const PropertyCell = styled.td` export const PropertyCell = styled.td`

View File

@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { transparentizeHex } from '../utils/styled';
import { PropertyNameCell } from './fields-layout'; import { PropertyNameCell } from './fields-layout';
import { transparentize } from 'polished';
export const ClickablePropertyNameCell = PropertyNameCell.extend` export const ClickablePropertyNameCell = PropertyNameCell.extend`
cursor: pointer; cursor: pointer;
@ -13,14 +13,14 @@ export const FieldLabel = styled.span`
`; `;
export const TypePrefix = styled(FieldLabel)` export const TypePrefix = styled(FieldLabel)`
color: ${props => transparentizeHex(props.theme.colors.text, 0.4)}; color: ${props => transparentize(0.4, props.theme.colors.text)};
`; `;
export const TypeName = styled(FieldLabel)` export const TypeName = styled(FieldLabel)`
color: ${props => transparentizeHex(props.theme.colors.text, 0.8)}; color: ${props => transparentize(0.8, props.theme.colors.text)};
`; `;
export const TypeTitle = styled(FieldLabel)` export const TypeTitle = styled(FieldLabel)`
color: ${props => transparentizeHex(props.theme.colors.text, 0.5)}; color: ${props => transparentize(0.5, props.theme.colors.text)};
`; `;
export const TypeFormat = TypeName; export const TypeFormat = TypeName;
@ -55,13 +55,13 @@ export const PatternLabel = styled(FieldLabel)`
export const ExampleValue = styled.span` export const ExampleValue = styled.span`
font-family: ${props => props.theme.code.fontFamily}; font-family: ${props => props.theme.code.fontFamily};
background-color: ${props => transparentizeHex(props.theme.colors.text, 0.02)}; background-color: ${props => transparentize(0.02, props.theme.colors.text)};
border: 1px solid ${props => transparentizeHex(props.theme.colors.text, 0.15)}; border: 1px solid ${props => transparentize(0.15, props.theme.colors.text)};
margin: 0 3px; margin: 0 3px;
padding: 0.4em 0.2em 0.2em; padding: 0.4em 0.2em 0.2em;
font-size: 0.8em; font-size: 0.8em;
border-radius: 2px; border-radius: 2px;
color: ${props => transparentizeHex(props.theme.colors.text, 0.9)}; color: ${props => transparentize(0.9, props.theme.colors.text)};
display: inline-block; display: inline-block;
min-width: 20px; min-width: 20px;
text-align: center; text-align: center;
@ -70,8 +70,8 @@ export const ExampleValue = styled.span`
`; `;
export const ConstraintItem = styled(FieldLabel)` export const ConstraintItem = styled(FieldLabel)`
background-color: ${props => transparentizeHex(props.theme.colors.main, 0.15)}; background-color: ${props => transparentize(0.15, props.theme.colors.main)};
color: ${props => transparentizeHex(props.theme.colors.main, 0.6)}; color: ${props => transparentize(0.6, props.theme.colors.main)};
margin-right: 6px; margin-right: 6px;
margin-left: 6px; margin-left: 6px;
border-radius: 2px; border-radius: 2px;

View File

@ -4,14 +4,3 @@ export const deprecatedCss = css`
text-decoration: line-through; text-decoration: line-through;
color: #bdccd3; color: #bdccd3;
`; `;
export const hoverColor = color => {
if (!color) {
return '';
}
return css`
&:hover {
color: ${color};
}
`;
};

View File

@ -1,7 +1,7 @@
import styled, { media } from '../styled-components'; import styled, { media } from '../styled-components';
export const MiddlePanel = styled.div` export const MiddlePanel = styled.div`
width: ${props => 100 - props.theme.rightPanel.width}%; width: calc(100% - ${props => props.theme.rightPanel.width});
padding: ${props => props.theme.spacingUnit * 2}px; padding: ${props => props.theme.spacingUnit * 2}px;
${media.lessThan('medium')` ${media.lessThan('medium')`
@ -10,9 +10,9 @@ export const MiddlePanel = styled.div`
`; `;
export const RightPanel = styled.div` export const RightPanel = styled.div`
width: ${props => props.theme.rightPanel.width}%; width: ${props => props.theme.rightPanel.width};
color: #fafbfc; color: #fafbfc;
bckground-color: ${props => props.theme.rightPanel.backgroundColor}; background-color: ${props => props.theme.rightPanel.backgroundColor};
padding: ${props => props.theme.spacingUnit * 2}px; padding: ${props => props.theme.spacingUnit * 2}px;
${media.lessThan('medium')` ${media.lessThan('medium')`

View File

@ -1,4 +1,3 @@
import { hoverColor } from '../../common-elements/mixins';
import styled, { media } from '../../styled-components'; import styled, { media } from '../../styled-components';
export { ClassAttributes } from 'react'; export { ClassAttributes } from 'react';
@ -29,8 +28,15 @@ export const RedocWrap = styled.div`
a { a {
text-decoration: none; text-decoration: none;
color: ${props => props.theme.links.color || props.theme.colors.main}; color: ${props => props.theme.links.color};
${props => hoverColor(props.theme.links.hover)};
&:visited {
color: ${props => props.theme.links.visited};
}
&:hover {
color: ${props => props.theme.links.hover};
}
} }
`; `;

View File

@ -1,7 +1,7 @@
import styled from '../../styled-components'; import styled from '../../styled-components';
import { UnderlinedHeader } from '../../common-elements'; import { UnderlinedHeader } from '../../common-elements';
import { transparentizeHex } from '../../utils'; import { transparentize } from 'polished';
import { ResponseTitle } from './ResponseTitle'; import { ResponseTitle } from './ResponseTitle';
export const StyledResponseTitle = styled(ResponseTitle)` export const StyledResponseTitle = styled(ResponseTitle)`
@ -13,7 +13,7 @@ export const StyledResponseTitle = styled(ResponseTitle)`
cursor: pointer; cursor: pointer;
color: ${props => props.theme.colors[props.type]}; color: ${props => props.theme.colors[props.type]};
background-color: ${props => transparentizeHex(props.theme.colors[props.type], 0.08)}; background-color: ${props => transparentize(0.08, props.theme.colors[props.type])};
${props => ${props =>
(props.empty && (props.empty &&

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { transparentizeHex } from '../../utils/styled'; import { transparentize } from 'polished';
import { UnderlinedHeader } from '../../common-elements/headers'; import { UnderlinedHeader } from '../../common-elements/headers';
import { SecurityRequirementModel } from '../../services/models/SecurityRequirement'; import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
@ -8,7 +8,7 @@ import { SecurityRequirementModel } from '../../services/models/SecurityRequirem
const ScopeName = styled.code` const ScopeName = styled.code`
font-size: ${props => props.theme.code.fontSize}; font-size: ${props => props.theme.code.fontSize};
font-family: ${props => props.theme.code.fontFamily}; font-family: ${props => props.theme.code.fontFamily};
border: 1px solid ${props => transparentizeHex(props.theme.colors.text, 0.15)}; border: 1px solid ${props => transparentize(0.15, props.theme.colors.text)};
margin: 0 3px; margin: 0 3px;
padding: 0.2em; padding: 0.2em;
display: inline-block; display: inline-block;

View File

@ -1,4 +1,4 @@
import defaultTheme, { ThemeInterface } from '../theme'; import defaultTheme, { ResolvedThemeInterface, ThemeInterface, resolveTheme } from '../theme';
import { querySelector } from '../utils/dom'; import { querySelector } from '../utils/dom';
import { isNumeric, mergeObjects } from '../utils/helpers'; import { isNumeric, mergeObjects } from '../utils/helpers';
@ -81,7 +81,7 @@ export class RedocNormalizedOptions {
return () => 0; return () => 0;
} }
theme: ThemeInterface; theme: ResolvedThemeInterface;
scrollYOffset: () => number; scrollYOffset: () => number;
hideHostname: boolean; hideHostname: boolean;
expandResponses: { [code: string]: boolean } | 'all'; expandResponses: { [code: string]: boolean } | 'all';
@ -93,7 +93,7 @@ export class RedocNormalizedOptions {
hideDownloadButton: boolean; hideDownloadButton: boolean;
constructor(raw: RedocRawOptions) { constructor(raw: RedocRawOptions) {
this.theme = mergeObjects({} as any, defaultTheme, raw.theme || {}); this.theme = resolveTheme(mergeObjects({} as any, defaultTheme, raw.theme || {}));
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset); this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname); this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses); this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);

View File

@ -1,8 +1,8 @@
import * as styledComponents from 'styled-components'; import * as styledComponents from 'styled-components';
import { ThemeInterface } from './theme'; import { ResolvedThemeInterface } from './theme';
export type StyledFunction<T> = styledComponents.ThemedStyledFunction<T, ThemeInterface>; export type StyledFunction<T> = styledComponents.ThemedStyledFunction<T, ResolvedThemeInterface>;
function withProps<T, U extends HTMLElement = HTMLElement>( function withProps<T, U extends HTMLElement = HTMLElement>(
styledFunction: StyledFunction<React.HTMLProps<U>>, styledFunction: StyledFunction<React.HTMLProps<U>>,
@ -19,7 +19,7 @@ const {
withTheme, withTheme,
} = (styledComponents as styledComponents.ThemedStyledComponentsModule< } = (styledComponents as styledComponents.ThemedStyledComponentsModule<
any any
>) as styledComponents.ThemedStyledComponentsModule<ThemeInterface>; >) as styledComponents.ThemedStyledComponentsModule<ResolvedThemeInterface>;
export const media = { export const media = {
lessThan(breakpoint) { lessThan(breakpoint) {
@ -40,9 +40,9 @@ export const media = {
between(firstBreakpoint, secondBreakpoint) { between(firstBreakpoint, secondBreakpoint) {
return (...args) => css` return (...args) => css`
@media (min-width: ${props => props.theme.breakpoints[firstBreakpoint]}) and (max-width: ${props => props.theme.breakpoints[ @media (min-width: ${props =>
secondBreakpoint props.theme.breakpoints[firstBreakpoint]}) and (max-width: ${props =>
]}) { props.theme.breakpoints[secondBreakpoint]}) {
${(css as any)(...args)}; ${(css as any)(...args)};
} }
`; `;

View File

@ -1,4 +1,6 @@
const theme = { import { lighten } from 'polished';
const theme: ThemeInterface = {
spacingUnit: 20, spacingUnit: 20,
breakpoints: { breakpoints: {
small: '50rem', small: '50rem',
@ -8,9 +10,9 @@ const theme = {
colors: { colors: {
main: '#32329f', main: '#32329f',
success: '#00aa13', success: '#00aa13',
redirect: 'orange', redirect: '#ffa500',
error: '#e53935', error: '#e53935',
info: 'skyblue', info: '#87ceeb',
text: '#263238', text: '#263238',
warning: '#f1c400', warning: '#f1c400',
http: { http: {
@ -44,9 +46,9 @@ const theme = {
fontFamily: 'Courier, monospace', fontFamily: 'Courier, monospace',
}, },
links: { links: {
color: undefined, // by default main color color: ({ colors }) => colors.main,
visited: undefined, // by default main color visited: ({ colors }) => colors.main,
hover: undefined, // by default main color hover: ({ colors }) => lighten(0.2, colors.main),
}, },
menu: { menu: {
width: '260px', width: '260px',
@ -58,10 +60,113 @@ const theme = {
}, },
rightPanel: { rightPanel: {
backgroundColor: '#263238', backgroundColor: '#263238',
width: 40, width: '40%',
}, },
}; };
export default theme; export default theme;
export type ThemeInterface = typeof theme; export function resolveTheme(theme: ThemeInterface): ResolvedThemeInterface {
const resolvedValues = {};
let counter = 0;
const setProxy = (obj, path: string) => {
Object.keys(obj).forEach(k => {
const currentPath = (path ? path + '.' : '') + k;
const val = obj[k];
if (typeof val === 'function') {
Object.defineProperty(obj, k, {
get() {
if (!resolvedValues[currentPath]) {
counter++;
if (counter > 1000) {
throw new Error(
`Theme probably contains cirucal dependency at ${currentPath}: ${val.toString()}`,
);
}
resolvedValues[currentPath] = val(theme);
}
return resolvedValues[currentPath];
},
enumerable: true,
});
} else if (typeof val === 'object') {
setProxy(val, currentPath);
}
});
};
setProxy(theme, '');
return JSON.parse(JSON.stringify(theme));
}
export interface ResolvedThemeInterface {
spacingUnit: number;
breakpoints: {
small: string;
medium: string;
large: string;
};
colors: {
main: string;
success: string;
redirect: string;
error: string;
info: string;
text: string;
warning: string;
http: {
get: string;
post: string;
put: string;
options: string;
patch: string;
delete: string;
basic: string;
link: string;
};
};
schemaView: {
linesColor: string;
defaultDetailsWidth: string;
};
baseFont: {
size: string;
lineHeight: string;
weight: string;
family: string;
smoothing: string;
optimizeSpeed: boolean;
};
headingsFont: {
family: string;
};
code: {
fontSize: string;
fontFamily: string;
};
links: {
color: string;
visited: string;
hover: string;
};
menu: {
width: string;
backgroundColor: string;
};
logo: {
maxHeight: string;
width: string;
};
rightPanel: {
backgroundColor: string;
width: string;
};
}
export type primitive = string | number | boolean | undefined | null;
export type AdvancedThemeDeep<T> = T extends primitive
? T | ((theme: ResolvedThemeInterface) => T)
: AdvancedThemeObject<T>;
export type AdvancedThemeObject<T> = { [P in keyof T]: AdvancedThemeDeep<T[P]> };
export type ThemeInterface = AdvancedThemeObject<ResolvedThemeInterface>;

View File

@ -1,19 +0,0 @@
import * as react from 'React';
import { transparentizeHex } from '../styled';
describe('transparentizeHex', () => {
test('simple transparentize', () => {
const res = transparentizeHex('#000000', 0.5);
expect(res).toBe('rgba(0, 0, 0, 0.5)');
});
test('transparentize hex shorthand', () => {
const res = transparentizeHex('#123', 0.5);
expect(res).toBe('rgba(17, 34, 51, 0.5)');
});
test('do not transparentize (withot last param)', () => {
const res = transparentizeHex('#010203');
expect(res).toBe('rgb(1, 2, 3)');
});
});

View File

@ -1,5 +1,4 @@
export * from './JsonPointer'; export * from './JsonPointer';
export * from './styled';
export * from './openapi'; export * from './openapi';
export * from './helpers'; export * from './helpers';

View File

@ -1,16 +0,0 @@
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
hex = hex.replace('#', '');
const r = parseInt(hex.length === 3 ? hex.slice(0, 1).repeat(2) : hex.slice(0, 2), 16);
const g = parseInt(hex.length === 3 ? hex.slice(1, 2).repeat(2) : hex.slice(2, 4), 16);
const b = parseInt(hex.length === 3 ? hex.slice(2, 3).repeat(2) : hex.slice(4, 6), 16);
return { r, g, b };
}
export function transparentizeHex(hex: string, alpha?: number): string {
const { r, g, b } = hexToRgb(hex);
if (alpha !== undefined) {
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
} else {
return `rgb(${r}, ${g}, ${b})`;
}
}

View File

@ -5972,6 +5972,10 @@ pn@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
polished@^1.9.2:
version "1.9.2"
resolved "https://registry.yarnpkg.com/polished/-/polished-1.9.2.tgz#d705cac66f3a3ed1bd38aad863e2c1e269baf6b6"
portfinder@^1.0.9: portfinder@^1.0.9:
version "1.0.13" version "1.0.13"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"