Skip to content

Commit 1df690a

Browse files
committed
feat: more advanced theme engine
1 parent ea3c731 commit 1df690a

15 files changed

+154
-85
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
"mobx-react": "^4.3.3",
100100
"openapi-sampler": "1.0.0-beta.9",
101101
"perfect-scrollbar": "^1.3.0",
102+
"polished": "^1.9.2",
102103
"prismjs": "^1.8.1",
103104
"prop-types": "^15.6.0",
104105
"react-dropdown": "^1.3.0",

src/common-elements/fields-layout.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import styled from '../styled-components';
2-
import { transparentizeHex } from '../utils/styled';
2+
import { transparentize } from 'polished';
33
import { deprecatedCss } from './mixins';
44

55
export const PropertiesTableCaption = styled.caption`
66
text-align: right;
77
font-size: 0.9em;
88
font-weight: normal;
9-
color: ${props => transparentizeHex(props.theme.colors.text, 0.4)};
9+
color: ${props => transparentize(0.4, props.theme.colors.text)};
1010
`;
1111

1212
export const PropertyCell = styled.td`

src/common-elements/fields.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import styled from 'styled-components';
2-
import { transparentizeHex } from '../utils/styled';
32
import { PropertyNameCell } from './fields-layout';
3+
import { transparentize } from 'polished';
44

55
export const ClickablePropertyNameCell = PropertyNameCell.extend`
66
cursor: pointer;
@@ -13,14 +13,14 @@ export const FieldLabel = styled.span`
1313
`;
1414

1515
export const TypePrefix = styled(FieldLabel)`
16-
color: ${props => transparentizeHex(props.theme.colors.text, 0.4)};
16+
color: ${props => transparentize(0.4, props.theme.colors.text)};
1717
`;
1818

1919
export const TypeName = styled(FieldLabel)`
20-
color: ${props => transparentizeHex(props.theme.colors.text, 0.8)};
20+
color: ${props => transparentize(0.8, props.theme.colors.text)};
2121
`;
2222
export const TypeTitle = styled(FieldLabel)`
23-
color: ${props => transparentizeHex(props.theme.colors.text, 0.5)};
23+
color: ${props => transparentize(0.5, props.theme.colors.text)};
2424
`;
2525

2626
export const TypeFormat = TypeName;
@@ -55,13 +55,13 @@ export const PatternLabel = styled(FieldLabel)`
5555

5656
export const ExampleValue = styled.span`
5757
font-family: ${props => props.theme.code.fontFamily};
58-
background-color: ${props => transparentizeHex(props.theme.colors.text, 0.02)};
59-
border: 1px solid ${props => transparentizeHex(props.theme.colors.text, 0.15)};
58+
background-color: ${props => transparentize(0.02, props.theme.colors.text)};
59+
border: 1px solid ${props => transparentize(0.15, props.theme.colors.text)};
6060
margin: 0 3px;
6161
padding: 0.4em 0.2em 0.2em;
6262
font-size: 0.8em;
6363
border-radius: 2px;
64-
color: ${props => transparentizeHex(props.theme.colors.text, 0.9)};
64+
color: ${props => transparentize(0.9, props.theme.colors.text)};
6565
display: inline-block;
6666
min-width: 20px;
6767
text-align: center;
@@ -70,8 +70,8 @@ export const ExampleValue = styled.span`
7070
`;
7171

7272
export const ConstraintItem = styled(FieldLabel)`
73-
background-color: ${props => transparentizeHex(props.theme.colors.main, 0.15)};
74-
color: ${props => transparentizeHex(props.theme.colors.main, 0.6)};
73+
background-color: ${props => transparentize(0.15, props.theme.colors.main)};
74+
color: ${props => transparentize(0.6, props.theme.colors.main)};
7575
margin-right: 6px;
7676
margin-left: 6px;
7777
border-radius: 2px;

src/common-elements/mixins.ts

-11
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,3 @@ export const deprecatedCss = css`
44
text-decoration: line-through;
55
color: #bdccd3;
66
`;
7-
8-
export const hoverColor = color => {
9-
if (!color) {
10-
return '';
11-
}
12-
return css`
13-
&:hover {
14-
color: ${color};
15-
}
16-
`;
17-
};

src/common-elements/panels.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import styled, { media } from '../styled-components';
22

33
export const MiddlePanel = styled.div`
4-
width: ${props => 100 - props.theme.rightPanel.width}%;
4+
width: calc(100% - ${props => props.theme.rightPanel.width});
55
padding: ${props => props.theme.spacingUnit * 2}px;
66
77
${media.lessThan('medium')`
@@ -10,9 +10,9 @@ export const MiddlePanel = styled.div`
1010
`;
1111

1212
export const RightPanel = styled.div`
13-
width: ${props => props.theme.rightPanel.width}%;
13+
width: ${props => props.theme.rightPanel.width};
1414
color: #fafbfc;
15-
bckground-color: ${props => props.theme.rightPanel.backgroundColor};
15+
background-color: ${props => props.theme.rightPanel.backgroundColor};
1616
padding: ${props => props.theme.spacingUnit * 2}px;
1717
1818
${media.lessThan('medium')`

src/components/Redoc/elements.tsx

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { hoverColor } from '../../common-elements/mixins';
21
import styled, { media } from '../../styled-components';
32
export { ClassAttributes } from 'react';
43

@@ -29,8 +28,15 @@ export const RedocWrap = styled.div`
2928
3029
a {
3130
text-decoration: none;
32-
color: ${props => props.theme.links.color || props.theme.colors.main};
33-
${props => hoverColor(props.theme.links.hover)};
31+
color: ${props => props.theme.links.color};
32+
33+
&:visited {
34+
color: ${props => props.theme.links.visited};
35+
}
36+
37+
&:hover {
38+
color: ${props => props.theme.links.hover};
39+
}
3440
}
3541
`;
3642

src/components/Responses/styled.elements.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import styled from '../../styled-components';
22

33
import { UnderlinedHeader } from '../../common-elements';
4-
import { transparentizeHex } from '../../utils';
4+
import { transparentize } from 'polished';
55
import { ResponseTitle } from './ResponseTitle';
66

77
export const StyledResponseTitle = styled(ResponseTitle)`
@@ -13,7 +13,7 @@ export const StyledResponseTitle = styled(ResponseTitle)`
1313
cursor: pointer;
1414
1515
color: ${props => props.theme.colors[props.type]};
16-
background-color: ${props => transparentizeHex(props.theme.colors[props.type], 0.08)};
16+
background-color: ${props => transparentize(0.08, props.theme.colors[props.type])};
1717
1818
${props =>
1919
(props.empty &&

src/components/SecurityRequirement/SecuirityRequirement.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as React from 'react';
22
import styled from '../../styled-components';
3-
import { transparentizeHex } from '../../utils/styled';
3+
import { transparentize } from 'polished';
44

55
import { UnderlinedHeader } from '../../common-elements/headers';
66
import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
77

88
const ScopeName = styled.code`
99
font-size: ${props => props.theme.code.fontSize};
1010
font-family: ${props => props.theme.code.fontFamily};
11-
border: 1px solid ${props => transparentizeHex(props.theme.colors.text, 0.15)};
11+
border: 1px solid ${props => transparentize(0.15, props.theme.colors.text)};
1212
margin: 0 3px;
1313
padding: 0.2em;
1414
display: inline-block;

src/services/RedocNormalizedOptions.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import defaultTheme, { ThemeInterface } from '../theme';
1+
import defaultTheme, { ResolvedThemeInterface, ThemeInterface, resolveTheme } from '../theme';
22
import { querySelector } from '../utils/dom';
33
import { isNumeric, mergeObjects } from '../utils/helpers';
44

@@ -81,7 +81,7 @@ export class RedocNormalizedOptions {
8181
return () => 0;
8282
}
8383

84-
theme: ThemeInterface;
84+
theme: ResolvedThemeInterface;
8585
scrollYOffset: () => number;
8686
hideHostname: boolean;
8787
expandResponses: { [code: string]: boolean } | 'all';
@@ -93,7 +93,7 @@ export class RedocNormalizedOptions {
9393
hideDownloadButton: boolean;
9494

9595
constructor(raw: RedocRawOptions) {
96-
this.theme = mergeObjects({} as any, defaultTheme, raw.theme || {});
96+
this.theme = resolveTheme(mergeObjects({} as any, defaultTheme, raw.theme || {}));
9797
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
9898
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);
9999
this.expandResponses = RedocNormalizedOptions.normalizeExpandResponses(raw.expandResponses);

src/styled-components.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as styledComponents from 'styled-components';
22

3-
import { ThemeInterface } from './theme';
3+
import { ResolvedThemeInterface } from './theme';
44

5-
export type StyledFunction<T> = styledComponents.ThemedStyledFunction<T, ThemeInterface>;
5+
export type StyledFunction<T> = styledComponents.ThemedStyledFunction<T, ResolvedThemeInterface>;
66

77
function withProps<T, U extends HTMLElement = HTMLElement>(
88
styledFunction: StyledFunction<React.HTMLProps<U>>,
@@ -19,7 +19,7 @@ const {
1919
withTheme,
2020
} = (styledComponents as styledComponents.ThemedStyledComponentsModule<
2121
any
22-
>) as styledComponents.ThemedStyledComponentsModule<ThemeInterface>;
22+
>) as styledComponents.ThemedStyledComponentsModule<ResolvedThemeInterface>;
2323

2424
export const media = {
2525
lessThan(breakpoint) {
@@ -40,9 +40,9 @@ export const media = {
4040

4141
between(firstBreakpoint, secondBreakpoint) {
4242
return (...args) => css`
43-
@media (min-width: ${props => props.theme.breakpoints[firstBreakpoint]}) and (max-width: ${props => props.theme.breakpoints[
44-
secondBreakpoint
45-
]}) {
43+
@media (min-width: ${props =>
44+
props.theme.breakpoints[firstBreakpoint]}) and (max-width: ${props =>
45+
props.theme.breakpoints[secondBreakpoint]}) {
4646
${(css as any)(...args)};
4747
}
4848
`;

src/theme.ts

+113-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
const theme = {
1+
import { lighten } from 'polished';
2+
3+
const theme: ThemeInterface = {
24
spacingUnit: 20,
35
breakpoints: {
46
small: '50rem',
@@ -8,9 +10,9 @@ const theme = {
810
colors: {
911
main: '#32329f',
1012
success: '#00aa13',
11-
redirect: 'orange',
13+
redirect: '#ffa500',
1214
error: '#e53935',
13-
info: 'skyblue',
15+
info: '#87ceeb',
1416
text: '#263238',
1517
warning: '#f1c400',
1618
http: {
@@ -44,9 +46,9 @@ const theme = {
4446
fontFamily: 'Courier, monospace',
4547
},
4648
links: {
47-
color: undefined, // by default main color
48-
visited: undefined, // by default main color
49-
hover: undefined, // by default main color
49+
color: ({ colors }) => colors.main,
50+
visited: ({ colors }) => colors.main,
51+
hover: ({ colors }) => lighten(0.2, colors.main),
5052
},
5153
menu: {
5254
width: '260px',
@@ -58,10 +60,113 @@ const theme = {
5860
},
5961
rightPanel: {
6062
backgroundColor: '#263238',
61-
width: 40,
63+
width: '40%',
6264
},
6365
};
6466

6567
export default theme;
6668

67-
export type ThemeInterface = typeof theme;
69+
export function resolveTheme(theme: ThemeInterface): ResolvedThemeInterface {
70+
const resolvedValues = {};
71+
let counter = 0;
72+
const setProxy = (obj, path: string) => {
73+
Object.keys(obj).forEach(k => {
74+
const currentPath = (path ? path + '.' : '') + k;
75+
const val = obj[k];
76+
if (typeof val === 'function') {
77+
Object.defineProperty(obj, k, {
78+
get() {
79+
if (!resolvedValues[currentPath]) {
80+
counter++;
81+
if (counter > 1000) {
82+
throw new Error(
83+
`Theme probably contains cirucal dependency at ${currentPath}: ${val.toString()}`,
84+
);
85+
}
86+
87+
resolvedValues[currentPath] = val(theme);
88+
}
89+
return resolvedValues[currentPath];
90+
},
91+
enumerable: true,
92+
});
93+
} else if (typeof val === 'object') {
94+
setProxy(val, currentPath);
95+
}
96+
});
97+
};
98+
99+
setProxy(theme, '');
100+
return JSON.parse(JSON.stringify(theme));
101+
}
102+
103+
export interface ResolvedThemeInterface {
104+
spacingUnit: number;
105+
breakpoints: {
106+
small: string;
107+
medium: string;
108+
large: string;
109+
};
110+
colors: {
111+
main: string;
112+
success: string;
113+
redirect: string;
114+
error: string;
115+
info: string;
116+
text: string;
117+
warning: string;
118+
http: {
119+
get: string;
120+
post: string;
121+
put: string;
122+
options: string;
123+
patch: string;
124+
delete: string;
125+
basic: string;
126+
link: string;
127+
};
128+
};
129+
schemaView: {
130+
linesColor: string;
131+
defaultDetailsWidth: string;
132+
};
133+
baseFont: {
134+
size: string;
135+
lineHeight: string;
136+
weight: string;
137+
family: string;
138+
smoothing: string;
139+
optimizeSpeed: boolean;
140+
};
141+
headingsFont: {
142+
family: string;
143+
};
144+
code: {
145+
fontSize: string;
146+
fontFamily: string;
147+
};
148+
links: {
149+
color: string;
150+
visited: string;
151+
hover: string;
152+
};
153+
menu: {
154+
width: string;
155+
backgroundColor: string;
156+
};
157+
logo: {
158+
maxHeight: string;
159+
width: string;
160+
};
161+
rightPanel: {
162+
backgroundColor: string;
163+
width: string;
164+
};
165+
}
166+
167+
export type primitive = string | number | boolean | undefined | null;
168+
export type AdvancedThemeDeep<T> = T extends primitive
169+
? T | ((theme: ResolvedThemeInterface) => T)
170+
: AdvancedThemeObject<T>;
171+
export type AdvancedThemeObject<T> = { [P in keyof T]: AdvancedThemeDeep<T[P]> };
172+
export type ThemeInterface = AdvancedThemeObject<ResolvedThemeInterface>;

src/utils/__tests__/styled.test.ts

-19
This file was deleted.

0 commit comments

Comments
 (0)