@react-spectrum/s2
Version:
Spectrum 2 UI components in React
824 lines (710 loc) • 29.6 kB
text/typescript
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import type {Condition, CSSProperties, CSSValue, CustomValue, Property, PropertyValueDefinition, PropertyValueMap, RenderProps, ShorthandProperty, StyleFunction, StyleValue, Theme, ThemeProperties, Value} from './types';
import fs from 'fs';
import * as propertyInfo from './properties.json';
// Postfix all class names with version for now.
const json = JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8'));
const POSTFIX = json.version.includes('nightly') ? json.version.match(/-nightly-(.*)/)[1] : json.version.replace(/[0.]/g, '');
export class ArbitraryProperty<T extends Value> implements Property<T> {
property: string;
toCSS: (value: T) => CSSValue;
constructor(property: string, toCSS?: (value: T) => CSSValue) {
this.property = property;
this.toCSS = toCSS || ((value) => String(value));
}
get cssProperties(): string[] {
return [this.property];
}
toCSSValue(value: T): PropertyValueDefinition<Value> {
return this.toCSS(value);
}
toCSSProperties(customProperty: string | null, value: PropertyValueDefinition<Value>): PropertyValueDefinition<[CSSProperties]> {
return mapConditionalValue(value, value => [{[customProperty || this.property]: String(value)}]);
}
}
export class MappedProperty<T extends CSSValue> extends ArbitraryProperty<T> implements Property<T> {
mapping: PropertyValueMap<T> | string[];
constructor(property: string, mapping: PropertyValueMap<T> | string[]) {
super(property);
this.mapping = mapping;
}
toCSSValue(value: T): PropertyValueDefinition<Value> {
if (Array.isArray(this.mapping)) {
if (!this.mapping.includes(String(value))) {
throw new Error('Invalid style value: ' + value);
}
return value;
} else {
let res = this.mapping[String(value)];
if (res == null) {
throw new Error('Invalid style value: ' + value);
}
return res;
}
}
}
export type Color<C extends string> = C | `${string}/${number}`;
export class ColorProperty<C extends string> extends MappedProperty<C> implements Property<Color<C>> {
toCSSValue(value: Color<C>): PropertyValueDefinition<Value> {
let [color, opacity] = value.split('/');
return mapConditionalValue(this.mapping[color], value => {
return opacity ? `rgb(from ${value} r g b / ${opacity}%)` : value;
});
}
}
export type LengthPercentageUnit = '%' | 'vw' | 'svw' | 'dvw' | 'vh' | 'svh' | 'dvh' | 'vmin' | 'svmin' | 'dvmin' | 'vmax' | 'svmax' | 'dvmax' | 'cqw' | 'cqh' | 'cqmin' | 'cqmax';
export type LengthPercentage = `${number}${LengthPercentageUnit}`;
export class PercentageProperty<T extends CSSValue> extends MappedProperty<T> implements Property<T | LengthPercentage> {
constructor(property: string, mapping: PropertyValueMap<T> | string[]) {
super(property, mapping);
}
toCSSValue(value: T | LengthPercentage): PropertyValueDefinition<Value> {
if (typeof value === 'string' && /^-?\d+(?:\.\d+)?(%|vw|svw|dvw|vh|svh|dvh|vmin|svmin|dvmin|vmax|svmax|dvmax|cqw|cqh|cqmin|cqmax)$/.test(value)) {
return value;
}
return super.toCSSValue(value as T);
}
}
export class SizingProperty<T extends CSSValue> extends PercentageProperty<T> implements Property<T | number | LengthPercentage> {
numberToCSS: (value: number) => string;
constructor(property: string, mapping: PropertyValueMap<T> | string[], numberToCSS: (value: number) => string) {
super(property, mapping);
this.numberToCSS = numberToCSS;
}
toCSSValue(value: T | LengthPercentage | number): PropertyValueDefinition<Value> {
if (typeof value === 'number') {
return value === 0 ? '0px' : this.numberToCSS(value);
}
return super.toCSSValue(value);
}
}
export class ExpandedProperty<T extends Value> implements Property<T> {
cssProperties: string[];
mapping: Property<T> | null;
expand: (v: T | CSSValue) => CSSProperties;
constructor(properties: string[], expand: (v: T | CSSValue) => CSSProperties, mapping?: Property<T> | PropertyValueMap<CSSValue>) {
this.cssProperties = properties;
this.expand = expand;
if (mapping instanceof MappedProperty) {
this.mapping = mapping;
} else if (mapping) {
this.mapping = new MappedProperty<any>(properties[0], mapping as any);
} else {
this.mapping = null;
}
}
toCSSValue(value: T): PropertyValueDefinition<Value> {
if (!this.mapping) {
return value;
}
return this.mapping.toCSSValue(value);
}
toCSSProperties(customProperty: string | null, value: PropertyValueDefinition<T>): PropertyValueDefinition<[CSSProperties]> {
if (customProperty) {
throw new Error('Style properties that expand into multiple CSS properties cannot be set as CSS variables.');
}
return mapConditionalValue(value, value => [this.expand(value)]);
}
}
function mapConditionalValue<T, U>(value: PropertyValueDefinition<T>, fn: (value: T) => U): PropertyValueDefinition<U> {
if (typeof value === 'object' && !Array.isArray(value)) {
let res: PropertyValueDefinition<U> = {};
for (let condition in value) {
res[condition] = mapConditionalValue((value as any)[condition], fn);
}
return res;
} else {
return fn(value);
}
}
function mapConditionalShorthand<T, C extends string, R extends RenderProps<string>>(value: PropertyValueDefinition<T>, fn: ShorthandProperty<T>): {[property: string]: StyleValue<Value, C, R>} {
if (typeof value === 'object') {
let res = {};
for (let condition in value) {
let properties = mapConditionalShorthand(value[condition], fn);
for (let property in properties) {
res[property] ??= {};
res[property][condition] = properties[property];
}
}
return res;
} else {
return fn(value);
}
}
export function parseArbitraryValue(value: Value): string | undefined {
if (typeof value === 'string' && value.startsWith('--')) {
return `var(${value})`;
} else if (typeof value === 'string' && value[0] === '[' && value[value.length - 1] === ']') {
return value.slice(1, -1);
} else if (
typeof value === 'string' && (
/^(var|calc|min|max|clamp|round|mod|rem|sin|cos|tan|asin|acos|atan|atan2|pow|sqrt|hypot|log|exp|abs|sign)\(.+\)$/.test(value) ||
/^(inherit|initial|unset)$/.test(value)
)
) {
return value;
}
}
function shortCSSPropertyName(property: string) {
return propertyInfo.properties[property] ?? generateArbitraryValueSelector(property, true);
}
function classNamePrefix(property: string, cssProperty: string) {
let className = propertyInfo.properties[cssProperty];
if (className && property === '--' + className) {
return '-' + className + '_-';
}
if (className && !property.startsWith('--')) {
return className;
}
return '-' + generateArbitraryValueSelector(property, true) + '-';
}
interface MacroContext {
addAsset(asset: {type: string, content: string}): void
}
export function createTheme<T extends Theme>(theme: T): StyleFunction<ThemeProperties<T>, 'default' | Extract<keyof T['conditions'], string>> {
let properties = new Map<string, Property<any>>(Object.entries(theme.properties).map(([k, v]) => {
if (!Array.isArray(v) && v.cssProperties) {
return [k, v as Property<any>];
}
return [k, new MappedProperty(k, v as any)];
}));
let dependencies = new Set<string>();
let hasConditions = false;
return function style(this: MacroContext | void, style, allowedOverrides?: readonly string[]) {
// Check if `this` is undefined, which means style was not called as a macro but as a normal function.
// We also check if this is globalThis, which happens in non-strict mode bundles.
// Also allow style to be called as a normal function in tests.
// @ts-ignore
// eslint-disable-next-line
if ((this == null || this === globalThis) && process.env.NODE_ENV !== 'test') {
throw new Error('The style macro must be imported with {type: "macro"}.');
}
// Generate rules for each property.
let rules = new Map<string, Rule>();
let values = new Map();
dependencies.clear();
let usedPriorities = 0;
let setRules = (key: string, value: [number, Rule[]]) => {
usedPriorities = Math.max(usedPriorities, value[0]);
rules.set(key, new GroupRule(value[1]));
};
hasConditions = false;
for (let key in style) {
let value = style[key]!;
let themeProperty = key;
values.set(key, value);
// Get the type of custom properties in the theme.
if (key.startsWith('--')) {
themeProperty = value.type;
value = value.value;
}
// Expand shorthands to longhands so that merging works as expected.
if (theme.shorthands[key]) {
let shorthand = theme.shorthands[key];
if (typeof shorthand === 'function') {
let expanded = mapConditionalShorthand(value, shorthand);
for (let k in expanded) {
let v = expanded[k];
values.set(k, v);
setRules(k, compileValue(k, k, v));
}
} else {
for (let prop of shorthand) {
values.set(prop, value);
setRules(prop, compileValue(prop, prop, value));
}
}
} else if (themeProperty in theme.properties) {
setRules(key, compileValue(key, themeProperty, value));
}
}
// For properties referenced by self(), rewrite the declarations to assign
// to an intermediary custom property so we can access the value.
for (let dep of dependencies) {
let value = values.get(dep);
if (value != null) {
if (!(dep in theme.properties)) {
throw new Error(`Unknown dependency ${dep}`);
}
let prop = properties.get(dep)!;
let name = `--${shortCSSPropertyName(prop.cssProperties[0])}`;
// Could potentially use @property to prevent the var from inheriting in children.
setRules(name, compileValue(name, dep, value));
setRules(dep, compileValue(dep, dep, name));
}
}
dependencies.clear();
let css = '';
// Declare layers for each priority ahead of time so the order is always correct.
css += '@layer ';
let first = true;
for (let i = 0; i <= usedPriorities; i++) {
if (first) {
first = false;
} else {
css += ', ';
}
css += layerName(generateName(i, true));
}
css += ';\n\n';
// If allowed overrides are provided, generate code to match the input override string and include only allowed classes.
// Also generate a variable for each overridable property that overlaps with the style definition. If those are defined,
// the defaults from the style definition are omitted.
let allowedOverridesSet = new Set<string>();
let js = 'let rules = " ";\n';
if (allowedOverrides?.length) {
for (let property of allowedOverrides) {
let shorthand = theme.shorthands[property];
let props = Array.isArray(shorthand) ? shorthand : [property];
for (let property of props) {
if (property.startsWith('--')) {
allowedOverridesSet.add(property);
continue;
}
let prop = properties.get(property);
if (!prop) {
throw new Error(`Invalid property ${property} in allowedOverrides`);
}
for (let property of prop.cssProperties) {
allowedOverridesSet.add(property);
}
}
}
let loop = '';
for (let property of rules.keys()) {
let prop = properties.get(property);
if (prop) {
for (let property of prop.cssProperties) {
if (property && allowedOverridesSet.has(property)) {
let selector = classNamePrefix(property, property);
let p = property.replace('--', '__');
js += `let ${p} = false;\n`;
loop += ` if (p[1] === ${JSON.stringify(selector)}) ${p} = true;\n`;
}
}
} else if (property.startsWith('--') && allowedOverridesSet.has(property)) {
let selector = classNamePrefix(property, property);
let p = property.replace('--', '__');
js += `let ${p} = false;\n`;
loop += ` if (p[1] === ${JSON.stringify(selector)}) ${p} = true;\n`;
}
}
let regex = `/(?:^|\\s)(${[...allowedOverridesSet].map(p => classNamePrefix(p, p)).join('|')})[^\\s]+/g`;
if (loop) {
js += `let matches = (overrides || '').matchAll(${regex});\n`;
js += 'for (let p of matches) {\n';
js += loop;
js += ' rules += p[0];\n';
js += '}\n';
} else {
js += `rules += ((overrides || '').match(${regex}) || []).join('')\n`;
}
}
// Generate JS and CSS for each rule.
let isStatic = !(hasConditions || allowedOverrides);
let className = '';
let rulesByLayer = new Map<string, string[]>();
let rootRule = new GroupRule([...rules.values()]);
if (isStatic) {
className += rootRule.getStaticClassName();
} else {
js += rootRule.toJS(allowedOverridesSet) + '\n';
}
rootRule.toCSS(rulesByLayer);
for (let [layer, rules] of rulesByLayer) {
css += `@layer ${layerName(layer)} {\n`;
css += rules.join('\n\n');
css += '}\n\n';
}
if (this && typeof this.addAsset === 'function') {
this.addAsset({
type: 'css',
content: css
});
}
if (isStatic) {
return className;
}
js += 'return rules;';
if (allowedOverrides) {
return new Function('props', 'overrides', js) as any;
}
return new Function('props', js) as any;
};
function compileValue(property: string, themeProperty: string, value: StyleValue<Value, Condition<T>, any>) {
return conditionalToRules(value as any, 0, new Set(), new Set(), (value, priority, conditions, skipConditions) => {
return compileRule(property, themeProperty, value, priority, conditions, skipConditions);
});
}
function conditionalToRules<P extends CustomValue | any[]>(
value: PropertyValueDefinition<P>,
parentPriority: number,
currentConditions: Set<string>,
skipConditions: Set<string>,
fn: (value: P, priority: number, conditions: Set<string>, skipConditions: Set<string>) => [number, Rule[]]
): [number, Rule[]] {
if (value && typeof value === 'object' && !Array.isArray(value)) {
let rules: Rule[] = [];
// Later conditions in parent rules override conditions in child rules.
let subSkipConditions = new Set([...skipConditions, ...Object.keys(value)]);
// Skip the default condition if we're already filtering by one of the other possible conditions.
// For example, if someone specifies `dark: 'gray-400'`, only include the dark version of `gray-400` from the theme.
let skipDefault = Object.keys(value).some(k => currentConditions.has(k));
let wasCSSCondition = false;
let priority = parentPriority;
for (let condition in value) {
if (skipConditions.has(condition) || (condition === 'default' && skipDefault)) {
continue;
}
subSkipConditions.delete(condition);
let val = value[condition];
// If a theme condition comes after runtime conditions, create a new grouping.
// This makes the CSS class unconditional so it appears outside the `else` block in the JS.
// The @layer order in the generated CSS will ensure that it overrides classes applied by runtime conditions.
let isCSSCondition = condition in theme.conditions || /^[@:]/.test(condition);
if (!wasCSSCondition && isCSSCondition && rules.length) {
rules = [new GroupRule(rules)];
}
wasCSSCondition = isCSSCondition;
// Increment the current priority whenever we see a new CSS condition.
if (isCSSCondition) {
priority++;
}
// If this is a runtime condition, inherit the priority from the parent rule.
// Otherwise, use the current maximum of the parent and current priorities.
let rulePriority = isCSSCondition ? priority : parentPriority;
if (condition === 'default' || isCSSCondition || /^is[A-Z]/.test(condition) || /^allows[A-Z]/.test(condition)) {
let subConditions = currentConditions;
if (isCSSCondition) {
subConditions = new Set([...currentConditions, condition]);
}
let [subPriority, subRules] = conditionalToRules(val, rulePriority, subConditions, subSkipConditions, fn);
rules.push(...compileCondition(currentConditions, condition, priority, subRules));
priority = Math.max(priority, subPriority);
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
for (let key in val) {
let [subPriority, subRules] = conditionalToRules(val[key], rulePriority, currentConditions, subSkipConditions, fn);
rules.push(...compileCondition(currentConditions, `${condition} === ${JSON.stringify(key)}`, priority, subRules));
priority = Math.max(priority, subPriority);
}
}
}
return [priority, rules];
} else {
// @ts-ignore - broken in non-strict?
return fn(value, parentPriority, currentConditions, skipConditions);
}
}
function compileCondition(conditions: Set<string>, condition: string, priority: number, rules: Rule[]): Rule[] {
if (condition === 'default' || conditions.has(condition)) {
return [new GroupRule(rules)];
}
if (condition in theme.conditions || /^[@:]/.test(condition)) {
// Conditions starting with : are CSS pseudo classes. Nest them inside the parent rule.
let prelude = theme.conditions[condition] || condition;
if (prelude.startsWith(':')) {
for (let rule of rules) {
rule.addPseudo(prelude);
}
return [new GroupRule(rules, generateName(priority, true))];
}
// Otherwise, wrap the rule in the condition (e.g. @media).
// Top level layer is based on the priority of the rule, not the condition.
// Also group in a sub-layer based on the condition so that lightningcss can more effectively deduplicate rules.
let layer = `${generateName(priority, true)}.${propertyInfo.conditions[theme.conditions[condition] || condition] || generateArbitraryValueSelector(condition, true)}`;
return [new AtRule(rules, prelude, layer)];
}
hasConditions = true;
return [new ConditionalRule(rules, condition)];
}
function compileRule(property: string, themeProperty: string, value: Value, priority: number, conditions: Set<string>, skipConditions: Set<string>): [number, Rule[]] {
let propertyFunction = properties.get(themeProperty);
if (propertyFunction) {
// Expand value to conditional CSS values, and then to rules.
let arbitrary = parseArbitraryValue(value);
let cssValue = arbitrary ? arbitrary : propertyFunction.toCSSValue(value);
let cssProperties = propertyFunction.toCSSProperties(property.startsWith('--') ? property : null, cssValue);
return conditionalToRules(cssProperties, priority, conditions, skipConditions, (value, priority, conditions) => {
let [obj] = value;
let rules: Rule[] = [];
for (let key in obj) {
let k = key as any;
let value = obj[k];
if (value === undefined) {
continue;
}
if (typeof value === 'string') {
// Replace self() references with variables and track the dependencies.
value = value.replace(/self\(([a-zA-Z]+)/g, (_, v) => {
let prop = properties.get(v);
if (!prop) {
throw new Error(`self(${v}) is invalid. ${v} is not a known property.`);
}
let cssProperties = prop.cssProperties;
if (cssProperties.length !== 1) {
throw new Error(`self(${v}) is not supported. ${v} expands to multiple CSS properties.`);
}
dependencies.add(v);
return `var(--${shortCSSPropertyName(cssProperties[0])}`;
});
}
// Generate selector. This consists of three parts: property, conditions, value.
let cssProperty = key;
if (property.startsWith('--')) {
cssProperty = propertyFunction.cssProperties[0];
}
let className = classNamePrefix(key, cssProperty);
if (conditions.size > 0) {
for (let condition of conditions) {
className += propertyInfo.conditions[theme.conditions[condition] || condition] || generateArbitraryValueSelector(condition);
}
}
if (cssProperty !== key) {
className += shortCSSPropertyName(cssProperty);
}
className += propertyInfo.values[cssProperty]?.[String(value)] ?? generateArbitraryValueSelector(String(value));
className += POSTFIX;
rules.push(new StyleRule(className, key, String(value)));
}
return [0, rules];
});
} else {
throw new Error('Unknown property ' + themeProperty);
}
}
}
function kebab(property: string) {
if (property.startsWith('--')) {
return property;
}
return property.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`);
}
// Generate a class name from a number, e.g. index within the theme.
// This maps to an alphabet containing lower case letters, upper case letters, and numbers.
// For numbers larger than 62, an underscore is prepended.
// This encoding allows easy parsing to enable runtime merging by property.
function generateName(index: number, atStart = false): string {
if (index < 26) {
// lower case letters
return String.fromCharCode(index + 97);
}
if (index < 52) {
// upper case letters
return String.fromCharCode((index - 26) + 65);
}
if (index < 62 && !atStart) {
// numbers
return String.fromCharCode((index - 52) + 48);
}
return '_' + generateName(index - (atStart ? 52 : 62));
}
// For arbitrary values, we use a hash of the string to generate the class name.
function generateArbitraryValueSelector(v: string, atStart = false) {
let c = toBase62(hash(v));
if (atStart && /^[0-9]/.test(c)) {
c = `_${c}`;
}
return c;
}
function toBase62(value: number) {
if (value === 0) {
return generateName(value);
}
let res = '';
while (value) {
let remainder = value % 62;
res += generateName(remainder);
value = Math.floor((value - remainder) / 62);
}
return res;
}
// djb2 hash function.
// http://www.cse.yorku.ca/~oz/hash.html
function hash(v: string) {
let hash = 5381;
for (let i = 0; i < v.length; i++) {
hash = ((hash << 5) + hash) + v.charCodeAt(i) >>> 0;
}
return hash;
}
function layerName(name: string) {
// All of our layers should be sub-layers of a single parent layer, so that
// the unsafe overrides layer always comes after.
return `_.${name}`;
}
interface Rule {
addPseudo(prelude: string): void,
getStaticClassName(): string,
toCSS(rulesByLayer: Map<string, string[]>, preludes?: string[], layer?: string): void,
toJS(allowedOverridesSet: Set<string>, indent?: string): string
}
/** A CSS style rule. */
class StyleRule implements Rule {
className: string;
pseudos: string;
property: string;
value: string;
constructor(className: string, property: string, value: string) {
this.className = className;
this.pseudos = '';
this.property = property;
this.value = value;
}
addPseudo(prelude: string) {
this.pseudos += prelude;
}
getStaticClassName(): string {
return ' ' + this.className;
}
toCSS(rulesByLayer: Map<string, string[]>, preludes: string[] = [], layer = 'a') {
let prelude = `.${this.className}${this.pseudos}`;
preludes.push(prelude);
// Nest rule in our stack of preludes (e.g. media queries/selectors).
let content = ' ';
preludes.forEach((p, i) => {
content += `${p} {\n${' '.repeat((i + 2) * 2)}`;
});
content += `${kebab(this.property)}: ${this.value};\n`;
preludes.map((_, i) => {
content += `${' '.repeat((preludes.length - i) * 2)}}\n`;
});
// Group rule into the appropriate layer.
let rules = rulesByLayer.get(layer);
if (!rules) {
rules = [];
rulesByLayer.set(layer, rules);
}
rules.push(content);
preludes.pop();
}
toJS(allowedOverridesSet: Set<string>, indent = ''): string {
let res = '';
if (allowedOverridesSet.has(this.property)) {
res += `${indent}if (!${this.property.replace('--', '__')}) `;
}
res += `${indent}rules += ' ${this.className}';`;
return res;
}
}
/** Base class for rules that contain other rules. */
class GroupRule implements Rule {
rules: Rule[];
layer: string | null;
constructor(rules: Rule[], layer?: string) {
this.rules = rules;
this.layer = layer ?? null;
}
addPseudo(prelude: string) {
for (let rule of this.rules) {
rule.addPseudo(prelude);
}
}
getStaticClassName(): string {
return this.rules.map(rule => rule.getStaticClassName()).join('');
}
toCSS(rulesByLayer: Map<string, string[]>, preludes?: string[], layer?: string) {
for (let rule of this.rules) {
rule.toCSS(rulesByLayer, preludes, this.layer || layer);
}
}
toJS(allowedOverridesSet: Set<string>, indent = ''): string {
let rules = this.rules.slice();
let conditional = rules.filter(rule => rule instanceof ConditionalRule).reverse().map((rule, i) => {
return `${i > 0 ? ' else ' : ''}${rule.toJS(allowedOverridesSet, indent)}`;
});
let elseCases = rules.filter(rule => !(rule instanceof ConditionalRule)).map(rule => rule.toJS(allowedOverridesSet, indent));
if (conditional.length && elseCases.length) {
return `${conditional.join('')} else {\n${indent} ${elseCases.join('\n' + indent + ' ')}\n${indent}}`;
}
if (conditional.length) {
return conditional.join('');
}
return elseCases.join('\n' + indent);
}
}
/** A rule that applies conditionally in CSS (e.g. @media). */
class AtRule extends GroupRule {
prelude: string;
constructor(rules: Rule[], prelude: string, layer: string) {
super(rules, layer);
this.prelude = prelude;
}
toCSS(rulesByLayer: Map<string, string[]>, preludes: string[] = [], layer?: string): void {
preludes.push(this.prelude);
super.toCSS(rulesByLayer, preludes, layer);
preludes?.pop();
}
}
/** A rule that applies conditionally at runtime. */
class ConditionalRule extends GroupRule {
condition: string;
constructor(rules: Rule[], condition: string) {
super(rules);
this.condition = condition;
}
getStaticClassName(): string {
throw new Error('Conditional rules cannot be compiled to a static class name. This is a bug.');
}
toJS(allowedOverridesSet: Set<string>, indent = ''): string {
return `${indent}if (props.${this.condition}) {\n${super.toJS(allowedOverridesSet, indent + ' ')}\n${indent}}`;
}
}
export function raw(this: MacroContext | void, css: string, layer = '_.a'): string {
// Check if `this` is undefined, which means style was not called as a macro but as a normal function.
// We also check if this is globalThis, which happens in non-strict mode bundles.
// Also allow style to be called as a normal function in tests.
// @ts-ignore
// eslint-disable-next-line
if ((this == null || this === globalThis) && process.env.NODE_ENV !== 'test') {
throw new Error('The raw macro must be imported with {type: "macro"}.');
}
let className = generateArbitraryValueSelector(css, true);
css = `@layer ${layer} {
.${className} {
${css}
}
}`;
// Ensure layer is always declared after the _ layer used by style macro.
if (!layer.startsWith('_.')) {
css = `@layer _, ${layer};\n` + css;
}
if (this && typeof this.addAsset === 'function') {
this.addAsset({
type: 'css',
content: css
});
}
return className;
}
export function keyframes(this: MacroContext | void, css: string): string {
// Check if `this` is undefined, which means style was not called as a macro but as a normal function.
// We also check if this is globalThis, which happens in non-strict mode bundles.
// Also allow style to be called as a normal function in tests.
// @ts-ignore
// eslint-disable-next-line
if ((this == null || this === globalThis) && process.env.NODE_ENV !== 'test') {
throw new Error('The keyframes macro must be imported with {type: "macro"}.');
}
let name = generateArbitraryValueSelector(css, true);
css = `@keyframes ${name} {
${css}
}`;
if (this && typeof this.addAsset === 'function') {
this.addAsset({
type: 'css',
content: css
});
}
return name;
}