forked from aws-amplify/amplify-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcreateTheme.ts
162 lines (148 loc) · 5.57 KB
/
createTheme.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// Internal Style Dictionary methods
import deepExtend from 'style-dictionary/lib/utils/deepExtend';
import flattenProperties from 'style-dictionary/lib/utils/flattenProperties';
import { defaultTheme } from './defaultTheme';
import { Theme, BaseTheme, WebTheme, Override } from './types';
import { cssValue, cssNameTransform } from './utils';
import { WebTokens } from './tokens';
import { DesignToken, WebDesignToken } from './tokens/types/designToken';
/**
* This will take a design token and add some data to it for it
* to be used in JS/CSS. It will create its CSS var name and update
* the value to use a CSS var if it is a reference. It will also
* add a `.toString()` method to make it easier to use in JS.
*
* We should see if there is a way to share this logic with style dictionary...
*/
function setupToken(token: DesignToken, path: Array<string>): WebDesignToken {
const name = `--${cssNameTransform({ path })}`;
const { value } = token;
return {
name,
path,
value: cssValue(token),
original: value,
toString: () => `var(${name})`,
};
}
/**
* Recursive function that will walk down the token object
* and perform the setupToken function on each token.
* Similar to what Style Dictionary does.
*/
function setupTokens(obj: any, path = []) {
let tokens = {};
if (obj.hasOwnProperty('value')) {
return setupToken(obj, path);
} else if (typeof obj === 'object') {
for (const name in obj) {
if (obj.hasOwnProperty(name)) {
if (typeof obj[name] !== 'object') {
// If we get to this point that means there is a 'dangling' part of the theme object
// basically some part of the theme object that is not a design token, which is
// anything that is not an object with a value attribute
console.warn(
`Non-design token found when creating the theme at path: ${path.join(
'.'
)}.${name}\nDid you forget to add '{value:"${obj[name]}"}'?`
);
// Keep the users data there just in case
tokens[name] = obj[name];
} else {
tokens[name] = setupTokens(obj[name], path.concat(name));
}
}
}
}
return tokens;
}
/**
* This will be used like `const myTheme = createTheme({})`
* `myTheme` can then be passed to a Provider or the generated CSS
* can be passed to a stylesheet at build-time or run-time.
* const myTheme = createTheme({})
* const myOtherTheme = createTheme({}, myTheme);
*/
export function createTheme(
theme?: Theme,
baseTheme: BaseTheme = defaultTheme
): WebTheme {
// merge theme and baseTheme to get a complete theme
// deepExtend is an internal Style Dictionary method
// that performs a deep merge on n objects. We could change
// this to another 3p deep merge solution too.
const mergedTheme: BaseTheme = deepExtend([{}, baseTheme, theme]);
// Setting up the tokens. This is similar to what Style Dictionary
// does. At the end of this, each token should have:
// - CSS variable name of itself
// - its value (reference to another CSS variable or raw value)
const tokens = setupTokens(mergedTheme.tokens) as WebTokens; // Setting the type here because setupTokens is recursive
const { breakpoints, name } = mergedTheme;
// flattenProperties is another internal Style Dictionary function
// that creates an array of all tokens.
let cssText =
`[data-amplify-theme="${name}"] {\n` +
flattenProperties(tokens)
.map((token) => `${token.name}: ${token.value};`)
.join('\n') +
`\n}\n`;
let overrides: Array<Override> = [];
/**
* For each override, we setup the tokens and then generate the CSS.
* This allows us to have one single CSS string for all possible overrides
* and avoid re-renders in React, but also support other frameworks as well.
*/
if (mergedTheme.overrides) {
overrides = mergedTheme.overrides.map((override) => {
const tokens = setupTokens(override.tokens);
const customProperties = flattenProperties(tokens)
.map((token) => `${token.name}: ${token.value};`)
.join('\n');
// Overrides can have a selector, media query, breakpoint, or color mode
// for creating the selector
if ('selector' in override) {
cssText += `\n${override.selector} {\n${customProperties}\n}\n`;
}
if ('mediaQuery' in override) {
cssText += `\n@media (${override.mediaQuery}) {
[data-amplify-theme="${name}"] {
${customProperties}
}
}\n`;
}
if ('breakpoint' in override) {
const breakpoint = mergedTheme.breakpoints.values[override.breakpoint];
cssText += `\n@media (min-width: ${breakpoint}px) {
[data-amplify-theme="${name}"] {
${customProperties}
}
}\n`;
}
if ('colorMode' in override) {
cssText += `\n@media (prefers-color-scheme: ${override.colorMode}) {
[data-amplify-theme="${name}"][data-amplify-color-mode="system"] {\n${customProperties}\n}
}\n`;
cssText += `\n[data-amplify-theme="${name}"][data-amplify-color-mode="${override.colorMode}"] {\n${customProperties}\n}\n`;
}
return {
...override,
tokens,
};
});
}
return {
tokens,
breakpoints,
name,
cssText,
// keep overrides separate from base theme
// this allows web platforms to use plain CSS scoped to a
// selector and only override the CSS vars needed. This
// means we could generate CSS at build-time in a postcss
// plugin, or do it at runtime and inject the CSS into a
// style tag.
// This also allows RN to dynamically switch themes in a
// provider.
overrides,
};
}