@react-spectrum/style-macro-s1 3.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +22 -0
- package/src/index.ts +14 -0
- package/src/runtime.ts +98 -0
- package/src/spectrum-theme.ts +874 -0
- package/src/style-macro.ts +481 -0
- package/src/types.ts +172 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
import type {Condition, CSSProperties, CSSValue, CustomValue, PropertyFunction, PropertyValueDefinition, PropertyValueMap, StyleFunction, StyleValue, Theme, ThemeProperties, Value} from './types';
|
|
13
|
+
|
|
14
|
+
export function createArbitraryProperty<T extends Value>(fn: (value: T, property: string) => CSSProperties): PropertyFunction<T> {
|
|
15
|
+
return (value, property) => {
|
|
16
|
+
let selector = Array.isArray(value) ? value.map(v => generateArbitraryValueSelector(String(v))).join('') : generateArbitraryValueSelector(String(value));
|
|
17
|
+
return {default: [fn(value, property), selector]};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createMappedProperty<T extends CSSValue>(fn: (value: string, property: string) => CSSProperties, values: PropertyValueMap<T> | string[]): PropertyFunction<T> {
|
|
22
|
+
let valueMap = createValueLookup(Array.isArray(values) ? values : Object.values(values).flatMap((v: any) => typeof v === 'object' ? Object.values(v) : [v]));
|
|
23
|
+
|
|
24
|
+
return (value, property) => {
|
|
25
|
+
let v = parseArbitraryValue(value);
|
|
26
|
+
if (v) {
|
|
27
|
+
return {default: [fn(v[0], property), v[1]]};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
let val = Array.isArray(values) ? value : values[String(value)];
|
|
32
|
+
return mapConditionalValue(val, value => {
|
|
33
|
+
return [fn(value, property), valueMap.get(value)!];
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Color<C extends string> = C | `${string}/${number}`;
|
|
39
|
+
export function createColorProperty<C extends string>(colors: PropertyValueMap<C>, property?: keyof CSSProperties): PropertyFunction<Color<C>> {
|
|
40
|
+
let valueMap = createValueLookup(Object.values(colors).flatMap((v: any) => typeof v === 'object' ? Object.values(v) : [v]));
|
|
41
|
+
return (value: Color<C>, key: string) => {
|
|
42
|
+
let v = parseArbitraryValue(value);
|
|
43
|
+
if (v) {
|
|
44
|
+
return {default: [{[property || key]: v[0]}, v[1]]};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let [color, opacity] = value.split('/');
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
let c = colors[color];
|
|
50
|
+
return mapConditionalValue(c, value => {
|
|
51
|
+
let css = opacity ? `rgb(from ${value} r g b / ${opacity}%)` : value;
|
|
52
|
+
let selector = valueMap.get(value)! + (opacity ? opacity.replace('.', '-') : '');
|
|
53
|
+
return [{[property || key]: css}, selector];
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mapConditionalValue<T, U>(value: PropertyValueDefinition<T>, fn: (value: T) => U): PropertyValueDefinition<U> {
|
|
59
|
+
if (typeof value === 'object') {
|
|
60
|
+
let res: PropertyValueDefinition<U> = {};
|
|
61
|
+
for (let condition in value) {
|
|
62
|
+
res[condition] = mapConditionalValue((value as any)[condition], fn);
|
|
63
|
+
}
|
|
64
|
+
return res;
|
|
65
|
+
} else {
|
|
66
|
+
return fn(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createValueLookup(values: Array<CSSValue>, atStart = false) {
|
|
71
|
+
let map = new Map<CSSValue, string>();
|
|
72
|
+
for (let value of values) {
|
|
73
|
+
if (!map.has(value)) {
|
|
74
|
+
map.set(value, generateName(map.size, atStart));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseArbitraryValue(value: any) {
|
|
81
|
+
if (typeof value === 'string' && value.startsWith('--')) {
|
|
82
|
+
return [`var(${value})`, value];
|
|
83
|
+
} else if (typeof value === 'string' && value[0] === '[' && value[value.length - 1] === ']') {
|
|
84
|
+
let s = generateArbitraryValueSelector(value.slice(1, -1));
|
|
85
|
+
return [value.slice(1, -1), s];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface MacroContext {
|
|
90
|
+
addAsset(asset: {type: string, content: string}): void
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createTheme<T extends Theme>(theme: T): StyleFunction<ThemeProperties<T>, 'default' | Extract<keyof T['conditions'], string>> {
|
|
94
|
+
let themePropertyMap = createValueLookup(Object.keys(theme.properties), true);
|
|
95
|
+
let themeConditionMap = createValueLookup(Object.keys(theme.conditions), true);
|
|
96
|
+
let propertyFunctions = new Map(Object.entries(theme.properties).map(([k, v]) => {
|
|
97
|
+
if (typeof v === 'function') {
|
|
98
|
+
return [k, v];
|
|
99
|
+
}
|
|
100
|
+
return [k, createMappedProperty((value, p) => ({[p]: value}), v) as PropertyFunction<Value>];
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
let dependencies = new Set<string>();
|
|
104
|
+
return function style(this: MacroContext | void, style) {
|
|
105
|
+
// Generate rules for each property.
|
|
106
|
+
let rules = new Map<string, Rule[]>();
|
|
107
|
+
let values = new Map();
|
|
108
|
+
dependencies.clear();
|
|
109
|
+
let usedPriorities = 1;
|
|
110
|
+
let setRules = (key: string, value: [number, Rule[]]) => {
|
|
111
|
+
usedPriorities = Math.max(usedPriorities, value[0]);
|
|
112
|
+
rules.set(key, value[1]);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
for (let key in style) {
|
|
116
|
+
let value: any = style[key]!;
|
|
117
|
+
let themeProperty = key;
|
|
118
|
+
values.set(key, value);
|
|
119
|
+
|
|
120
|
+
// Get the type of custom properties in the theme.
|
|
121
|
+
if (key.startsWith('--')) {
|
|
122
|
+
themeProperty = value.type;
|
|
123
|
+
value = value.value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Expand shorthands to longhands so that merging works as expected.
|
|
127
|
+
if (theme.shorthands[key]) {
|
|
128
|
+
for (let prop of theme.shorthands[key]) {
|
|
129
|
+
values.set(prop, value);
|
|
130
|
+
setRules(prop, compileValue(prop, prop, value));
|
|
131
|
+
}
|
|
132
|
+
} else if (themeProperty in theme.properties) {
|
|
133
|
+
setRules(key, compileValue(key, themeProperty, value));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// For properties referenced by self(), rewrite the declarations to assign
|
|
138
|
+
// to an intermediary custom property so we can access the value.
|
|
139
|
+
for (let dep of dependencies) {
|
|
140
|
+
let value = values.get(dep);
|
|
141
|
+
if (value != null) {
|
|
142
|
+
if (!(dep in theme.properties)) {
|
|
143
|
+
throw new Error(`Unknown dependency ${dep}`);
|
|
144
|
+
}
|
|
145
|
+
let name = `--${themePropertyMap.get(dep)}`;
|
|
146
|
+
// Could potentially use @property to prevent the var from inheriting in children.
|
|
147
|
+
setRules(name, compileValue(name, dep, value));
|
|
148
|
+
setRules(dep, compileValue(dep, dep, name));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
dependencies.clear();
|
|
152
|
+
|
|
153
|
+
// Declare layers for each priority ahead of time so the order is always correct.
|
|
154
|
+
let css = '@layer ';
|
|
155
|
+
let first = true;
|
|
156
|
+
for (let i = 0; i <= usedPriorities; i++) {
|
|
157
|
+
if (first) {
|
|
158
|
+
first = false;
|
|
159
|
+
} else {
|
|
160
|
+
css += ', ';
|
|
161
|
+
}
|
|
162
|
+
css += generateName(i, true);
|
|
163
|
+
}
|
|
164
|
+
css += ';\n\n';
|
|
165
|
+
|
|
166
|
+
// Generate JS and CSS for each rule.
|
|
167
|
+
let js = 'let rules = "";\n';
|
|
168
|
+
for (let propertyRules of rules.values()) {
|
|
169
|
+
js += printJS(propertyRules) + '\n';
|
|
170
|
+
for (let rule of propertyRules) {
|
|
171
|
+
css += printRule(rule) + '\n\n';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
js += 'return rules;';
|
|
176
|
+
|
|
177
|
+
if (this && typeof this?.addAsset === 'function') {
|
|
178
|
+
this.addAsset({
|
|
179
|
+
type: 'css',
|
|
180
|
+
content: css
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return new Function('props', js) as any;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function compileValue(property: string, themeProperty: string, value: StyleValue<Value, Condition<T>, any>) {
|
|
188
|
+
return conditionalToRules(value as any, 0, new Set(), new Set(), (value, priority, conditions, skipConditions) => {
|
|
189
|
+
return compileRule(property, themeProperty, value, priority, conditions, skipConditions);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function conditionalToRules<P extends CustomValue | any[]>(
|
|
194
|
+
value: PropertyValueDefinition<P>,
|
|
195
|
+
parentPriority: number,
|
|
196
|
+
currentConditions: Set<string>,
|
|
197
|
+
skipConditions: Set<string>,
|
|
198
|
+
fn: (value: P, priority: number, conditions: Set<string>, skipConditions: Set<string>) => [number, Rule[]]
|
|
199
|
+
): [number, Rule[]] {
|
|
200
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
201
|
+
let rules: Rule[] = [];
|
|
202
|
+
|
|
203
|
+
// Later conditions in parent rules override conditions in child rules.
|
|
204
|
+
let subSkipConditions = new Set([...skipConditions, ...Object.keys(value)]);
|
|
205
|
+
|
|
206
|
+
// Skip the default condition if we're already filtering by one of the other possible conditions.
|
|
207
|
+
// For example, if someone specifies `dark: 'gray-400'`, only include the dark version of `gray-400` from the theme.
|
|
208
|
+
let skipDefault = Object.keys(value).some(k => currentConditions.has(k));
|
|
209
|
+
let wasCSSCondition = false;
|
|
210
|
+
let priority = parentPriority;
|
|
211
|
+
|
|
212
|
+
for (let condition in value) {
|
|
213
|
+
if (skipConditions.has(condition) || (condition === 'default' && skipDefault)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
subSkipConditions.delete(condition);
|
|
217
|
+
|
|
218
|
+
let val = value[condition];
|
|
219
|
+
|
|
220
|
+
// If a theme condition comes after runtime conditions, create a new grouping.
|
|
221
|
+
// This makes the CSS class unconditional so it appears outside the `else` block in the JS.
|
|
222
|
+
// The @layer order in the generated CSS will ensure that it overrides classes applied by runtime conditions.
|
|
223
|
+
let isCSSCondition = condition in theme.conditions || /^[@:]/.test(condition);
|
|
224
|
+
if (!wasCSSCondition && isCSSCondition && rules.length) {
|
|
225
|
+
rules = [{prelude: '', condition: '', body: rules}];
|
|
226
|
+
}
|
|
227
|
+
wasCSSCondition = isCSSCondition;
|
|
228
|
+
|
|
229
|
+
// Increment the current priority whenever we see a new CSS condition.
|
|
230
|
+
if (isCSSCondition) {
|
|
231
|
+
priority++;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If this is a runtime condition, inherit the priority from the parent rule.
|
|
235
|
+
// Otherwise, use the current maximum of the parent and current priorities.
|
|
236
|
+
let rulePriority = isCSSCondition ? priority : parentPriority;
|
|
237
|
+
|
|
238
|
+
if (condition === 'default' || isCSSCondition || /^is[A-Z]/.test(condition) || /^allows[A-Z]/.test(condition)) {
|
|
239
|
+
let subConditions = currentConditions;
|
|
240
|
+
if (isCSSCondition) {
|
|
241
|
+
subConditions = new Set([...currentConditions, condition]);
|
|
242
|
+
}
|
|
243
|
+
let [subPriority, subRules] = conditionalToRules(val, rulePriority, subConditions, subSkipConditions, fn);
|
|
244
|
+
rules.push(...compileCondition(currentConditions, condition, priority, subRules));
|
|
245
|
+
priority = Math.max(priority, subPriority);
|
|
246
|
+
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
247
|
+
for (let key in val) {
|
|
248
|
+
let [subPriority, subRules] = conditionalToRules(val[key], rulePriority, currentConditions, subSkipConditions, fn);
|
|
249
|
+
rules.push(...compileCondition(currentConditions, `${condition} === ${JSON.stringify(key)}`, priority, subRules));
|
|
250
|
+
priority = Math.max(priority, subPriority);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return [priority, rules];
|
|
255
|
+
} else {
|
|
256
|
+
return fn(value as any, parentPriority, currentConditions, skipConditions);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function compileCondition(conditions: Set<string>, condition: string, priority: number, rules: Rule[]): Rule[] {
|
|
261
|
+
if (condition === 'default' || conditions.has(condition)) {
|
|
262
|
+
return [{prelude: '', condition: '', body: rules}];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (condition in theme.conditions || /^[@:]/.test(condition)) {
|
|
266
|
+
// Conditions starting with : are CSS pseudo classes. Nest them inside the parent rule.
|
|
267
|
+
let prelude = theme.conditions[condition] || condition;
|
|
268
|
+
if (prelude.startsWith(':')) {
|
|
269
|
+
return [{
|
|
270
|
+
prelude: `@layer ${generateName(priority, true)}`,
|
|
271
|
+
body: rules.map(rule => ({prelude: rule.prelude, body: [{...rule, prelude: '&' + prelude}], condition: ''})),
|
|
272
|
+
condition: ''
|
|
273
|
+
}];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Otherwise, wrap the rule in the condition (e.g. @media).
|
|
277
|
+
return [{
|
|
278
|
+
// Top level layer is based on the priority of the rule, not the condition.
|
|
279
|
+
// Also group in a sub-layer based on the condition so that lightningcss can more effectively deduplicate rules.
|
|
280
|
+
prelude: `@layer ${generateName(priority, true)}.${themeConditionMap.get(condition) || generateArbitraryValueSelector(condition, true)}`,
|
|
281
|
+
body: [{prelude, body: rules, condition: ''}],
|
|
282
|
+
condition: ''
|
|
283
|
+
}];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return [{prelude: '', condition, body: rules}];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function compileRule(property: string, themeProperty: string, value: Value, priority: number, conditions: Set<string>, skipConditions: Set<string>): [number, Rule[]] {
|
|
290
|
+
// Generate selector. This consists of three parts:
|
|
291
|
+
// 1. Property. For custom properties we use a hash. For theme properties, we use the index within the theme.
|
|
292
|
+
// 2. Conditions. This uses the index within the theme.
|
|
293
|
+
// 3. Value. The index in the theme, or a hash for arbitrary values.
|
|
294
|
+
let prelude = '.';
|
|
295
|
+
if (property.startsWith('--')) {
|
|
296
|
+
prelude += generateArbitraryValueSelector(property, true) + '-';
|
|
297
|
+
} else {
|
|
298
|
+
prelude += themePropertyMap.get(themeProperty);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let propertyFunction = propertyFunctions.get(themeProperty);
|
|
302
|
+
if (propertyFunction) {
|
|
303
|
+
// Expand value to conditional CSS values, and then to rules.
|
|
304
|
+
let res = propertyFunction(value, property);
|
|
305
|
+
return conditionalToRules(res, priority, conditions, skipConditions, (value, priority, conditions) => {
|
|
306
|
+
let [obj, p] = value;
|
|
307
|
+
let body = '';
|
|
308
|
+
for (let key in obj) {
|
|
309
|
+
let k = key as any;
|
|
310
|
+
let value = obj[k];
|
|
311
|
+
if (value === undefined) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (typeof value === 'string') {
|
|
315
|
+
// Replace self() references with variables and track the dependencies.
|
|
316
|
+
value = value.replace(/self\(([a-zA-Z]+)/g, (_, v) => {
|
|
317
|
+
dependencies.add(v);
|
|
318
|
+
return `var(--${themePropertyMap.get(v)}`;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
body += `${kebab(key)}: ${value};`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let selector = prelude;
|
|
325
|
+
if (conditions.size > 0) {
|
|
326
|
+
for (let condition of conditions) {
|
|
327
|
+
selector += themeConditionMap.get(condition) || generateArbitraryValueSelector(condition);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let rules: Rule[] = [{
|
|
332
|
+
condition: '',
|
|
333
|
+
prelude: selector + p,
|
|
334
|
+
body
|
|
335
|
+
}];
|
|
336
|
+
|
|
337
|
+
if (conditions.size === 0) {
|
|
338
|
+
rules = [{
|
|
339
|
+
prelude: '@layer a',
|
|
340
|
+
body: rules,
|
|
341
|
+
condition: ''
|
|
342
|
+
}];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return [0, rules];
|
|
346
|
+
});
|
|
347
|
+
} else {
|
|
348
|
+
throw new Error('Unknown property ' + themeProperty);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function kebab(property: string) {
|
|
354
|
+
if (property.startsWith('--')) {
|
|
355
|
+
return property;
|
|
356
|
+
}
|
|
357
|
+
return property.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
interface Rule {
|
|
361
|
+
prelude: string,
|
|
362
|
+
condition: string,
|
|
363
|
+
body: string | Rule[]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Generate a class name from a number, e.g. index within the theme.
|
|
367
|
+
// This maps to an alphabet containing lower case letters, upper case letters, and numbers.
|
|
368
|
+
// For numbers larger than 62, an underscore is prepended.
|
|
369
|
+
// This encoding allows easy parsing to enable runtime merging by property.
|
|
370
|
+
function generateName(index: number, atStart = false): string {
|
|
371
|
+
if (index < 26) {
|
|
372
|
+
// lower case letters
|
|
373
|
+
return String.fromCharCode(index + 97);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (index < 52) {
|
|
377
|
+
// upper case letters
|
|
378
|
+
return String.fromCharCode((index - 26) + 65);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (index < 62) {
|
|
382
|
+
// numbers
|
|
383
|
+
let res = String.fromCharCode((index - 52) + 48);
|
|
384
|
+
if (atStart) {
|
|
385
|
+
res = '_' + res;
|
|
386
|
+
}
|
|
387
|
+
return res;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return '_' + generateName(index - 62);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// For arbitrary values, we use a hash of the string to generate the class name.
|
|
394
|
+
function generateArbitraryValueSelector(v: string, atStart = false) {
|
|
395
|
+
let c = hash(v).toString(36);
|
|
396
|
+
if (atStart && /^[0-9]/.test(c)) {
|
|
397
|
+
c = `_${c}`;
|
|
398
|
+
}
|
|
399
|
+
return `-${c}`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// djb2 hash function.
|
|
403
|
+
// http://www.cse.yorku.ca/~oz/hash.html
|
|
404
|
+
function hash(v: string) {
|
|
405
|
+
let hash = 5381;
|
|
406
|
+
for (let i = 0; i < v.length; i++) {
|
|
407
|
+
hash = ((hash << 5) + hash) + v.charCodeAt(i) >>> 0;
|
|
408
|
+
}
|
|
409
|
+
return hash;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function printRule(rule: Rule, indent = ''): string {
|
|
413
|
+
if (!rule.prelude && typeof rule.body !== 'string') {
|
|
414
|
+
return rule.body.map(b => printRule(b, indent)).join('\n\n');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return `${indent}${rule.prelude} {
|
|
418
|
+
${typeof rule.body === 'string' ? indent + ' ' + rule.body : rule.body.map(b => printRule(b, indent + ' ')).join('\n\n')}
|
|
419
|
+
${indent}}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function printJS(rules: Rule[], indent = ''): string {
|
|
423
|
+
rules = rules.slice().reverse();
|
|
424
|
+
|
|
425
|
+
let conditional = rules.filter(rule => rule.condition).map((rule, i) => {
|
|
426
|
+
return `${i > 0 ? ' else ' : ''}if (props.${rule.condition}) {\n${indent} ${printRuleChildren(rule, indent + ' ')}\n${indent}}`;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
let elseCases = rules.filter(rule => !rule.condition).map(rule => printRuleChildren(rule, indent + ' '));
|
|
430
|
+
|
|
431
|
+
if (conditional.length && elseCases.length) {
|
|
432
|
+
return `${conditional.join('')} else {\n${indent} ${elseCases.join('\n' + indent + ' ')}\n${indent}}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (conditional.length) {
|
|
436
|
+
return conditional.join('');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return elseCases.join('\n' + indent);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function printRuleChildren(rule: Rule, indent = '') {
|
|
443
|
+
let res = '';
|
|
444
|
+
if (rule.prelude.startsWith('.')) {
|
|
445
|
+
res += `rules += ' ${rule.prelude.slice(1)}';`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (Array.isArray(rule.body)) {
|
|
449
|
+
res += printJS(rule.body, indent);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return res;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export function raw(this: MacroContext | void, css: string) {
|
|
456
|
+
let className = generateArbitraryValueSelector(css, true);
|
|
457
|
+
css = `.${className} {
|
|
458
|
+
${css}
|
|
459
|
+
}`;
|
|
460
|
+
if (this && typeof this?.addAsset === 'function') {
|
|
461
|
+
this.addAsset({
|
|
462
|
+
type: 'css',
|
|
463
|
+
content: css
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return className;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function keyframes(this: MacroContext | void, css: string) {
|
|
470
|
+
let name = generateArbitraryValueSelector(css, true);
|
|
471
|
+
css = `@keyframes ${name} {
|
|
472
|
+
${css}
|
|
473
|
+
}`;
|
|
474
|
+
if (this && typeof this?.addAsset === 'function') {
|
|
475
|
+
this.addAsset({
|
|
476
|
+
type: 'css',
|
|
477
|
+
content: css
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return name;
|
|
481
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
import type * as CSS from 'csstype';
|
|
13
|
+
|
|
14
|
+
export type CSSValue = string | number;
|
|
15
|
+
export type CustomValue = string | number | boolean;
|
|
16
|
+
export type Value = CustomValue | readonly CustomValue[];
|
|
17
|
+
export type PropertyValueDefinition<T> = T | {[condition: string]: PropertyValueDefinition<T>};
|
|
18
|
+
export type PropertyValueMap<T extends CSSValue = CSSValue> = {
|
|
19
|
+
[name in T]: PropertyValueDefinition<string>
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CustomProperty = `--${string}`;
|
|
23
|
+
export type CSSProperties = CSS.Properties & {
|
|
24
|
+
[k: CustomProperty]: CSSValue
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PropertyFunction<T extends Value> = (value: T, property: string) => PropertyValueDefinition<[CSSProperties, string]>;
|
|
28
|
+
|
|
29
|
+
export interface Theme {
|
|
30
|
+
properties: {
|
|
31
|
+
[name: string]: PropertyValueMap | PropertyFunction<any> | string[]
|
|
32
|
+
},
|
|
33
|
+
conditions: {
|
|
34
|
+
[name: string]: string
|
|
35
|
+
},
|
|
36
|
+
shorthands: {
|
|
37
|
+
[name: string]: string[]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type PropertyValue<T> =
|
|
42
|
+
T extends PropertyFunction<infer P>
|
|
43
|
+
? P
|
|
44
|
+
: T extends PropertyValueMap<infer P>
|
|
45
|
+
? P
|
|
46
|
+
: T extends string[]
|
|
47
|
+
? T[number]
|
|
48
|
+
: never;
|
|
49
|
+
|
|
50
|
+
type PropertyValue2<T> = PropertyValue<T> | CustomProperty | `[${string}]`;
|
|
51
|
+
type Merge<T> = T extends any ? T : never;
|
|
52
|
+
|
|
53
|
+
// Pre-compute value types for all theme properties ahead of time.
|
|
54
|
+
export type ThemeProperties<T extends Theme> = Merge<{
|
|
55
|
+
[K in keyof T['properties'] | keyof T['shorthands']]: K extends keyof T['properties']
|
|
56
|
+
? Merge<PropertyValue2<T['properties'][K]>>
|
|
57
|
+
: Merge<PropertyValue2<T['properties'][T['shorthands'][K][0]]>>
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
type Style<T extends ThemeProperties<Theme>, C extends string, R extends RenderProps<string>> =
|
|
61
|
+
StaticProperties<T, C, R> & CustomProperties<T, C, R>;
|
|
62
|
+
|
|
63
|
+
type StaticProperties<T extends ThemeProperties<Theme>, C extends string, R extends RenderProps<string>> = {
|
|
64
|
+
[Name in keyof T]?: StyleValue<T[Name], C, R>
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type CustomProperties<T extends ThemeProperties<Theme>, C extends string, R extends RenderProps<string>> = {
|
|
68
|
+
[key: CustomProperty]: CustomPropertyValue<T, keyof T, C, R>
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Infer the value type of custom property values from the `type` key, which references a theme property.
|
|
72
|
+
type CustomPropertyValue<T extends ThemeProperties<Theme>, P extends keyof T, C extends string, R extends RenderProps<string>> =
|
|
73
|
+
P extends any
|
|
74
|
+
? {type: P, value: StyleValue<T[P], C, R>}
|
|
75
|
+
: never;
|
|
76
|
+
|
|
77
|
+
export type RenderProps<K extends string> = {
|
|
78
|
+
[key in K]: any
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type StyleValue<V extends Value, C extends string, R extends RenderProps<string>> = V | Conditional<V, C, R>;
|
|
82
|
+
export type Condition<T extends Theme> = 'default' | Extract<keyof T['conditions'], string>;
|
|
83
|
+
type Conditional<V extends Value, C extends string, R extends RenderProps<string>> =
|
|
84
|
+
CSSConditions<V, C, R> & RuntimeConditions<V, C, R>
|
|
85
|
+
|
|
86
|
+
type ArbitraryCondition = `:${string}` | `@${string}`;
|
|
87
|
+
type CSSConditions<V extends Value, C extends string, R extends RenderProps<string>> = {
|
|
88
|
+
[name in C]?: StyleValue<V, C, R>
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// If render props are unknown, allow any custom conditions to be inferred.
|
|
92
|
+
// Unfortunately this breaks "may only specify known properties" errors.
|
|
93
|
+
type RuntimeConditions<V extends Value, C extends string, R extends RenderProps<string>> =
|
|
94
|
+
[R] extends [never]
|
|
95
|
+
? UnknownConditions<V, C>
|
|
96
|
+
: RenderPropConditions<V, C, R>;
|
|
97
|
+
|
|
98
|
+
type UnknownConditions<V extends Value, C extends string> = {
|
|
99
|
+
[name: string]: StyleValue<V, C, never> | VariantMap<string, V, C, never>
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type BooleanConditionName = `is${Capitalize<string>}` | `allows${Capitalize<string>}`;
|
|
103
|
+
type RenderPropConditions<V extends Value, C extends string, R extends RenderProps<string>> = {
|
|
104
|
+
[K in keyof R]?: K extends BooleanConditionName ? StyleValue<V, C, R> : VariantMap<R[K], V, C, R>
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type Values<T, K extends keyof T = keyof T> = {
|
|
108
|
+
[k in K]: T[k]
|
|
109
|
+
}[K];
|
|
110
|
+
|
|
111
|
+
export type VariantMap<K extends CSSValue, V extends Value, C extends string, R extends RenderProps<string>> = {
|
|
112
|
+
[k in K]?: StyleValue<V, C, R>
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// These types are used to recursively extract all runtime conditions/variants in case an
|
|
116
|
+
// explicit render prop generic type is not provided/inferred. This allows the returned function
|
|
117
|
+
// to automatically accept the correct arguments based on the style definition.
|
|
118
|
+
type ExtractConditionalValue<C extends keyof any, V> = V extends Value
|
|
119
|
+
? never
|
|
120
|
+
// Add the keys from this level for boolean conditions not in the theme.
|
|
121
|
+
: RuntimeConditionObject<Extract<keyof V, BooleanConditionName>, boolean>
|
|
122
|
+
// Add variant values for non-boolean named keys.
|
|
123
|
+
| Variants<V, Exclude<keyof V, C | BooleanConditionName>>
|
|
124
|
+
// Recursively include conditions from the next level.
|
|
125
|
+
| ExtractConditionalValue<C,
|
|
126
|
+
| Values<V, Extract<keyof V, C | BooleanConditionName>>
|
|
127
|
+
// And skip over variants to get to the values.
|
|
128
|
+
| Values<Values<V, Exclude<keyof V, C | BooleanConditionName>>>
|
|
129
|
+
>;
|
|
130
|
+
|
|
131
|
+
type RuntimeConditionObject<K, V> = K extends keyof any ? { [P in K]?: V } : never;
|
|
132
|
+
|
|
133
|
+
type Variants<T, K extends keyof T> = K extends any ? {
|
|
134
|
+
[k in K]?: keyof T[k]
|
|
135
|
+
} : never;
|
|
136
|
+
|
|
137
|
+
type InferCustomPropertyValue<T> = T extends {value: infer V} ? V : never;
|
|
138
|
+
|
|
139
|
+
// https://stackoverflow.com/questions/49401866/all-possible-keys-of-an-union-type
|
|
140
|
+
type KeysOfUnion<T> = T extends T ? keyof T: never;
|
|
141
|
+
type KeyValue<T, K extends KeysOfUnion<T>> = T extends {[k in K]?: any} ? T[K] : never;
|
|
142
|
+
type MergeUnion<T> = { [K in KeysOfUnion<T>]: KeyValue<T, K> };
|
|
143
|
+
|
|
144
|
+
type RuntimeConditionsObject<C extends keyof any, S extends Style<any, any, any>> = MergeUnion<
|
|
145
|
+
ExtractConditionalValue<C,
|
|
146
|
+
| Values<S, Exclude<keyof S, CustomProperty>>
|
|
147
|
+
// Skip top-level object for custom properties and go straight to value.
|
|
148
|
+
| InferCustomPropertyValue<Values<S, Extract<keyof S, CustomProperty>>>
|
|
149
|
+
>
|
|
150
|
+
>;
|
|
151
|
+
|
|
152
|
+
// Return an intersection between string and the used style props so we can prevent passing certain properties to components.
|
|
153
|
+
type IncludedProperties<S> = Merge<{
|
|
154
|
+
[K in keyof S]: unknown
|
|
155
|
+
}>;
|
|
156
|
+
type Keys<R> = [R] extends [never] ? never : keyof R;
|
|
157
|
+
export type RuntimeStyleFunction<S, R> = Keys<R> extends never ? () => string & S : (props: R) => string & S;
|
|
158
|
+
|
|
159
|
+
// If an render prop type was provided, use that so that we get autocomplete for conditions.
|
|
160
|
+
// Otherwise, fall back to inferring the render props from the style definition itself.
|
|
161
|
+
type InferProps<R, C extends keyof any, S extends Style<any, any, any>> = [R] extends [never] ? AllowOthers<RuntimeConditionsObject<C, S>> : R;
|
|
162
|
+
type AllowOthers<R> = Keys<R> extends never ? never : R | {[x: string]: any}
|
|
163
|
+
export type StyleFunction<T extends ThemeProperties<Theme>, C extends string> =
|
|
164
|
+
<R extends RenderProps<string> = never, S extends Style<T, C, R> = Style<T, C | ArbitraryCondition, R>>(style: S) => RuntimeStyleFunction<IncludedProperties<S>, InferProps<R, C | ArbitraryCondition, S>>;
|
|
165
|
+
|
|
166
|
+
// Creates a version of ThemeProperties with excluded keys mapped to never.
|
|
167
|
+
// This allows creating a component prop that only accepts certain style props.
|
|
168
|
+
type LimitTheme<T, P> = Merge<{
|
|
169
|
+
[K in keyof T]?: K extends P ? unknown : never
|
|
170
|
+
}>;
|
|
171
|
+
|
|
172
|
+
export type CSSProp<S, P extends string | number> = S extends StyleFunction<infer T, any> ? string & LimitTheme<T, P> : never;
|