From ced7ad23fb663388637400ad86b496f6aed1abd2 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 28 Jan 2026 12:15:59 -0600 Subject: [PATCH 01/17] export all utilities --- packages/@react-spectrum/s2/src/index.ts | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 0865c16c0cb..7f48c6e0f25 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -91,6 +91,24 @@ export {TreeView, TreeViewItem, TreeViewItemContent, TreeViewLoadMoreItem} from export {pressScale} from './pressScale'; +export { + getAllowedOverrides, + field, + fieldLabel, + fieldInput, + control, + controlFont, + controlSize, + controlBorderRadius, + centerPadding, + colorScheme, + staticColor, + widthProperties, + heightProperties +} from './style-utils'; + +export {focusRing, iconStyle} from '../style'; + export {Autocomplete, Collection, FileTrigger, parseColor, useLocale} from 'react-aria-components'; export {useListData, useTreeData, useAsyncList} from 'react-stately'; @@ -168,3 +186,12 @@ export type {TooltipProps} from './Tooltip'; export type {TreeViewProps, TreeViewItemProps, TreeViewItemContentProps, TreeViewLoadMoreItemProps} from './TreeView'; export type {AutocompleteProps, FileTriggerProps, TooltipTriggerComponentProps as TooltipTriggerProps, SortDescriptor, Color, Key, Selection, RouterConfig} from 'react-aria-components'; export type {ListData, TreeData, AsyncListData} from 'react-stately'; + +export type { + StylesProp, + StylesPropWithHeight, + StylesPropWithoutWidth, + UnsafeClassName, + UnsafeStyles, + StyleProps +} from './style-utils'; From 418056f528701169d373b18e1d8db8e803602056 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 28 Jan 2026 13:54:11 -0600 Subject: [PATCH 02/17] export remaining utils --- packages/@react-spectrum/s2/src/index.ts | 3 ++- packages/@react-spectrum/s2/style/index.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 7f48c6e0f25..5469aef78de 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -107,7 +107,8 @@ export { heightProperties } from './style-utils'; -export {focusRing, iconStyle} from '../style'; +export {focusRing, iconStyle, linearGradient, edgeToText, raw, keyframes} from '../style'; +export {mergeStyles} from '../style/runtime'; export {Autocomplete, Collection, FileTrigger, parseColor, useLocale} from 'react-aria-components'; export {useListData, useTreeData, useAsyncList} from 'react-stately'; diff --git a/packages/@react-spectrum/s2/style/index.ts b/packages/@react-spectrum/s2/style/index.ts index 7a847469bd7..2cefa419782 100644 --- a/packages/@react-spectrum/s2/style/index.ts +++ b/packages/@react-spectrum/s2/style/index.ts @@ -15,7 +15,8 @@ import {Inset, fontRelative as internalFontRelative, space as internalSpace, Spa import type {MacroContext} from '@parcel/macros'; import {StyleString} from './types'; -export {baseColor, color, lightDark, colorMix, size, style} from './spectrum-theme'; +export {baseColor, color, lightDark, colorMix, size, style, linearGradient, edgeToText} from './spectrum-theme'; +export {raw, keyframes} from './style-macro'; export type {StyleString} from './types'; // Wrap these functions in arbitrary value syntax when called from the outside. From fe2bc928665b3ee324ae15b93cd2deded80ef3be Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 09:24:23 -0600 Subject: [PATCH 03/17] removed unsafe exports for now --- packages/@react-spectrum/s2/src/index.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 5469aef78de..e08dc1412d1 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -93,21 +93,11 @@ export {pressScale} from './pressScale'; export { getAllowedOverrides, - field, - fieldLabel, - fieldInput, - control, - controlFont, - controlSize, - controlBorderRadius, centerPadding, - colorScheme, - staticColor, - widthProperties, - heightProperties + colorScheme } from './style-utils'; -export {focusRing, iconStyle, linearGradient, edgeToText, raw, keyframes} from '../style'; +export {raw, keyframes} from '../style'; export {mergeStyles} from '../style/runtime'; export {Autocomplete, Collection, FileTrigger, parseColor, useLocale} from 'react-aria-components'; From 4e620c34f60b98cee97bb1e7ae9904e7cb3fbbbf Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 10:01:47 -0600 Subject: [PATCH 04/17] add JSDoc descriptions --- .../@react-spectrum/s2/src/style-utils.ts | 23 +++++++++++ packages/@react-spectrum/s2/style/index.ts | 19 ++++++++- packages/@react-spectrum/s2/style/runtime.ts | 15 +++++++ .../@react-spectrum/s2/style/style-macro.ts | 39 +++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index 3c743afee54..3885eff7702 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -14,6 +14,14 @@ import {CSSProperties} from 'react'; import {fontRelative} from '../style'; import {StyleString} from '../style/types'; +/** + * Calculates vertical padding to center a single line of text within a container. + * Uses the CSS `self()` function and `1lh` unit to compute the padding based on + * the container's minimum height and border widths. + * + * @param minHeight - A CSS expression for the minimum height to center within. Defaults to `'self(minHeight)'`. + * @returns A CSS `calc()` expression wrapped as an arbitrary style value. + */ export function centerPadding(minHeight: string = 'self(minHeight)'): `[${string}]` { return `[calc((${minHeight} - self(borderTopWidth, 0px) - self(borderBottomWidth, 0px) - 1lh) / 2)]`; } @@ -113,6 +121,11 @@ export const fieldInput = () => ({ containIntrinsicWidth: 'calc(var(--defaultWidth) - self(paddingStart, 0px) - self(paddingEnd, 0px) - self(borderStartWidth, 0px) - self(borderEndWidth, 0px))' } as const); +/** + * Returns style properties that configure the CSS `color-scheme` for a component. + * Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values + * via the `colorScheme` render prop condition. + */ export const colorScheme = () => ({ colorScheme: { // Default to page color scheme if none is defined. @@ -335,6 +348,16 @@ export interface StyleProps extends UnsafeStyles { styles?: StylesProp } +/** + * Returns the list of CSS property names that are allowed as style overrides via the `styles` prop. + * By default includes layout properties (margin, position, grid, etc.) and width properties. + * Optionally includes height properties. + * + * @param options - Configuration for which property groups to include. + * @param options.width - Whether to include width properties (`width`, `minWidth`, `maxWidth`). Defaults to `true`. + * @param options.height - Whether to include height properties (`height`, `minHeight`, `maxHeight`, `size`). Defaults to `false`. + * @returns An array of allowed CSS property names. + */ export function getAllowedOverrides({width = true, height = false} = {}): string[] { return (allowedOverrides as unknown as string[]).concat(width ? widthProperties : []).concat(height ? heightProperties : []); } diff --git a/packages/@react-spectrum/s2/style/index.ts b/packages/@react-spectrum/s2/style/index.ts index 2cefa419782..b815479cb34 100644 --- a/packages/@react-spectrum/s2/style/index.ts +++ b/packages/@react-spectrum/s2/style/index.ts @@ -15,7 +15,7 @@ import {Inset, fontRelative as internalFontRelative, space as internalSpace, Spa import type {MacroContext} from '@parcel/macros'; import {StyleString} from './types'; -export {baseColor, color, lightDark, colorMix, size, style, linearGradient, edgeToText} from './spectrum-theme'; +export {baseColor, color, lightDark, colorMix, size, style} from './spectrum-theme'; export {raw, keyframes} from './style-macro'; export type {StyleString} from './types'; @@ -24,6 +24,23 @@ export function space(px: number): `[${string}]` { return `[${internalSpace(px)}]`; } +/** + * Converts a pixel value to a font-relative `em` length. Useful for sizing elements + * relative to the current font size. + * + * @param base - The pixel value to convert. + * @param baseFontSize - The base font size in pixels to divide by. Defaults to `14`. + * @returns A CSS `em` value wrapped as an arbitrary style value. + * + * @example + * ```tsx + * import {fontRelative} from '@react-spectrum/s2/style' with {type: 'macro'}; + * + * const className = style({ + * gap: fontRelative(2) // 2/14 = ~0.143em + * }); + * ``` + */ export function fontRelative(base: number, baseFontSize?: number): `[${string}]` { return `[${internalFontRelative(base, baseFontSize)}]`; } diff --git a/packages/@react-spectrum/s2/style/runtime.ts b/packages/@react-spectrum/s2/style/runtime.ts index 370fcb7b675..96d375c8f5b 100644 --- a/packages/@react-spectrum/s2/style/runtime.ts +++ b/packages/@react-spectrum/s2/style/runtime.ts @@ -36,6 +36,21 @@ import {StyleString} from './types'; // }; // } +/** + * Merges multiple style strings together, combining the CSS properties from each. + * Later styles take precedence over earlier ones for the same property. + * Useful for composing styles from multiple `style()` macro calls. + * + * @example + * ```tsx + * import {mergeStyles} from '@react-spectrum/s2'; + * import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * + * const baseStyles = style({padding: 8}); + * const overrideStyles = style({padding: 16, color: 'heading'}); + * const merged = mergeStyles(baseStyles, overrideStyles); + * ``` + */ export function mergeStyles(...styles: (StyleString | null | undefined)[]): StyleString { let definedStyles = styles.filter(Boolean) as StyleString[]; if (definedStyles.length === 1) { diff --git a/packages/@react-spectrum/s2/style/style-macro.ts b/packages/@react-spectrum/s2/style/style-macro.ts index 8286f4dfa8a..512ac202f44 100644 --- a/packages/@react-spectrum/s2/style/style-macro.ts +++ b/packages/@react-spectrum/s2/style/style-macro.ts @@ -863,6 +863,24 @@ class ConditionalRule extends GroupRule { } } +/** + * Injects a raw CSS string into the style system. The CSS is wrapped in a generated + * class name and placed within the specified `@layer`. Returns the generated class name. + * Must be imported with `{type: 'macro'}`. + * + * @param css - The raw CSS declarations to inject. + * @param layer - The CSS `@layer` to place the styles in. Defaults to `'_.a'`. + * @returns The generated class name that applies the styles. + * + * @example + * ```tsx + * import {raw} from '@react-spectrum/s2/style' with {type: 'macro'}; + * + * const className = raw(` + * backdrop-filter: blur(8px); + * `); + * ``` + */ 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. @@ -893,6 +911,27 @@ export function raw(this: MacroContext | void, css: string, layer = '_.a'): stri return className; } +/** + * Defines a CSS `@keyframes` animation and returns the generated animation name. + * Must be imported with `{type: 'macro'}`. + * + * @param css - The keyframe rules (e.g. `from { ... } to { ... }`). + * @returns The generated animation name to use in CSS `animation` properties. + * + * @example + * ```tsx + * import {keyframes} from '@react-spectrum/s2/style' with {type: 'macro'}; + * + * const fadeIn = keyframes(` + * from { opacity: 0; } + * to { opacity: 1; } + * `); + * + * const className = style({ + * animation: fadeIn, + * }); + * ``` + */ 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. From 0cf5ad700a6c0bec2135926e96b97c549eea1d5c Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 10:15:35 -0600 Subject: [PATCH 05/17] rename colorScheme() to setColorScheme() --- packages/@react-spectrum/s2/src/CoachMark.tsx | 4 ++-- packages/@react-spectrum/s2/src/Modal.tsx | 4 ++-- packages/@react-spectrum/s2/src/Popover.tsx | 4 ++-- packages/@react-spectrum/s2/src/Provider.tsx | 4 ++-- packages/@react-spectrum/s2/src/TableView.tsx | 4 ++-- packages/@react-spectrum/s2/src/Tooltip.tsx | 4 ++-- packages/@react-spectrum/s2/src/index.ts | 2 +- packages/@react-spectrum/s2/src/style-utils.ts | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/@react-spectrum/s2/src/CoachMark.tsx b/packages/@react-spectrum/s2/src/CoachMark.tsx index d84dde75282..def76c6a1a2 100644 --- a/packages/@react-spectrum/s2/src/CoachMark.tsx +++ b/packages/@react-spectrum/s2/src/CoachMark.tsx @@ -27,7 +27,7 @@ import { import {ButtonContext} from './Button'; import {Card} from './Card'; import {CheckboxContext} from './Checkbox'; -import {colorScheme, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; +import {setColorScheme, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {ColorSchemeContext} from './Provider'; import {ContentContext, FooterContext, KeyboardContext, TextContext} from './Content'; import { @@ -106,7 +106,7 @@ const slideLeftKeyframes = keyframes(` `); let popover = style({ - ...colorScheme(), + ...setColorScheme(), '--s2-container-bg': { type: 'backgroundColor', value: 'layer-2' diff --git a/packages/@react-spectrum/s2/src/Modal.tsx b/packages/@react-spectrum/s2/src/Modal.tsx index a7927be2941..fc9e1c6bbd6 100644 --- a/packages/@react-spectrum/s2/src/Modal.tsx +++ b/packages/@react-spectrum/s2/src/Modal.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {colorScheme} from './style-utils' with {type: 'macro'}; +import {setColorScheme} from './style-utils' with {type: 'macro'}; import {ColorSchemeContext} from './Provider'; import {DOMRef, GlobalDOMAttributes} from '@react-types/shared'; import {forwardRef, MutableRefObject, useCallback, useContext} from 'react'; @@ -28,7 +28,7 @@ interface ModalProps extends Omit({ - ...colorScheme(), + ...setColorScheme(), justifyContent: 'center', alignItems: 'center', maxWidth: 160, diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index e08dc1412d1..d620d424cd7 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -94,7 +94,7 @@ export {pressScale} from './pressScale'; export { getAllowedOverrides, centerPadding, - colorScheme + setColorScheme } from './style-utils'; export {raw, keyframes} from '../style'; diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index 3885eff7702..0f9a493caa7 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -122,11 +122,11 @@ export const fieldInput = () => ({ } as const); /** - * Returns style properties that configure the CSS `color-scheme` for a component. + * Returns style properties that set the CSS `color-scheme` for a component. * Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values * via the `colorScheme` render prop condition. */ -export const colorScheme = () => ({ +export const setColorScheme = () => ({ colorScheme: { // Default to page color scheme if none is defined. default: '[var(--lightningcss-light, light) var(--lightningcss-dark, dark)]', From acf0f38f7852efd7e5a801c1c7cbbb45b2928067 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 10:21:49 -0600 Subject: [PATCH 06/17] remove raw/keyframes from s2 export --- packages/@react-spectrum/s2/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index d620d424cd7..2e01c66e9d6 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -97,7 +97,6 @@ export { setColorScheme } from './style-utils'; -export {raw, keyframes} from '../style'; export {mergeStyles} from '../style/runtime'; export {Autocomplete, Collection, FileTrigger, parseColor, useLocale} from 'react-aria-components'; From 9f6eb4e78c71557a88e95da927776e83e5b10234 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 10:22:51 -0600 Subject: [PATCH 07/17] export WidthProperties and HeightProperties as types --- packages/@react-spectrum/s2/src/index.ts | 4 +++- packages/@react-spectrum/s2/src/style-utils.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index 2e01c66e9d6..dd57f36bc6d 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -183,5 +183,7 @@ export type { StylesPropWithoutWidth, UnsafeClassName, UnsafeStyles, - StyleProps + StyleProps, + WidthProperties, + HeightProperties } from './style-utils'; diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index 0f9a493caa7..ac3efcc6a76 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -325,6 +325,9 @@ export const widthProperties = [ 'maxWidth' ] as const; +/** The set of width-related CSS property names (`width`, `minWidth`, `maxWidth`). */ +export type WidthProperties = (typeof widthProperties)[number]; + export const heightProperties = [ 'size', 'height', @@ -332,6 +335,9 @@ export const heightProperties = [ 'maxHeight' ] as const; +/** The set of height-related CSS property names (`size`, `height`, `minHeight`, `maxHeight`). */ +export type HeightProperties = (typeof heightProperties)[number]; + export type StylesProp = StyleString<(typeof allowedOverrides)[number] | (typeof widthProperties)[number]>; export type StylesPropWithHeight = StyleString<(typeof allowedOverrides)[number] | (typeof widthProperties)[number] | (typeof heightProperties)[number]>; export type StylesPropWithoutWidth = StyleString<(typeof allowedOverrides)[number]>; From 2034cc092f7829c651e32cc2c25f716d31f10a4c Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 10:41:50 -0600 Subject: [PATCH 08/17] cleanup JSDocs --- .../@react-spectrum/s2/src/style-utils.ts | 27 +++++ packages/@react-spectrum/s2/style/index.ts | 44 +++++++- .../s2/style/spectrum-theme.ts | 100 ++++++++++++++++++ .../@react-spectrum/s2/style/style-macro.ts | 4 +- 4 files changed, 171 insertions(+), 4 deletions(-) diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index ac3efcc6a76..4de89803f10 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -21,6 +21,13 @@ import {StyleString} from '../style/types'; * * @param minHeight - A CSS expression for the minimum height to center within. Defaults to `'self(minHeight)'`. * @returns A CSS `calc()` expression wrapped as an arbitrary style value. + * + * @example + * ```tsx + * const styles = style({ + * paddingY: centerPadding() + * }); + * ``` */ export function centerPadding(minHeight: string = 'self(minHeight)'): `[${string}]` { return `[calc((${minHeight} - self(borderTopWidth, 0px) - self(borderBottomWidth, 0px) - 1lh) / 2)]`; @@ -125,6 +132,14 @@ export const fieldInput = () => ({ * Returns style properties that set the CSS `color-scheme` for a component. * Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values * via the `colorScheme` render prop condition. + * + * @example + * ```tsx + * const styles = style({ + * ...setColorScheme(), + * backgroundColor: 'layer-1' + * }); + * ``` */ export const setColorScheme = () => ({ colorScheme: { @@ -363,6 +378,18 @@ export interface StyleProps extends UnsafeStyles { * @param options.width - Whether to include width properties (`width`, `minWidth`, `maxWidth`). Defaults to `true`. * @param options.height - Whether to include height properties (`height`, `minHeight`, `maxHeight`, `size`). Defaults to `false`. * @returns An array of allowed CSS property names. + * + * @example + * ```tsx + * const styles = style({ + * // ... component styles + * }, getAllowedOverrides()); + * + * // With height overrides enabled: + * const styles = style({ + * // ... component styles + * }, getAllowedOverrides({height: true})); + * ``` */ export function getAllowedOverrides({width = true, height = false} = {}): string[] { return (allowedOverrides as unknown as string[]).concat(width ? widthProperties : []).concat(height ? heightProperties : []); diff --git a/packages/@react-spectrum/s2/style/index.ts b/packages/@react-spectrum/s2/style/index.ts index b815479cb34..6fb6291c9ec 100644 --- a/packages/@react-spectrum/s2/style/index.ts +++ b/packages/@react-spectrum/s2/style/index.ts @@ -19,7 +19,21 @@ export {baseColor, color, lightDark, colorMix, size, style} from './spectrum-the export {raw, keyframes} from './style-macro'; export type {StyleString} from './types'; -// Wrap these functions in arbitrary value syntax when called from the outside. +/** + * Converts a pixel value to a Spectrum spacing token in `rem` units. + * + * @param px - The spacing in pixels. + * @returns A `rem` value wrapped as an arbitrary style value. + * + * @example + * ```tsx + * import {space} from '@react-spectrum/s2/style' with {type: 'macro'}; + * + * const styles = style({ + * gap: space(12) + * }); + * ``` + */ export function space(px: number): `[${string}]` { return `[${internalSpace(px)}]`; } @@ -36,7 +50,7 @@ export function space(px: number): `[${string}]` { * ```tsx * import {fontRelative} from '@react-spectrum/s2/style' with {type: 'macro'}; * - * const className = style({ + * const styles = style({ * gap: fontRelative(2) // 2/14 = ~0.143em * }); * ``` @@ -45,6 +59,17 @@ export function fontRelative(base: number, baseFontSize?: number): `[${string}]` return `[${internalFontRelative(base, baseFontSize)}]`; } +/** + * Returns consistent Spectrum focus ring outline styles for interactive components. + * + * @example + * ```tsx + * const styles = style({ + * ...focusRing(), + * borderRadius: 'lg' + * }); + * ``` + */ export const focusRing = () => ({ outlineStyle: { default: 'none', @@ -96,6 +121,21 @@ const iconSizes = { XL: 26 } as const; +/** + * Generates styles for an icon element with the given size, color, and layout options. + * Must be imported with `{type: 'macro'}`. + * + * @param options - Icon styling options including `size` (XS–XL), `color`, and layout properties. + * @returns A `StyleString` that can be applied to an icon element. + * + * @example + * ```tsx + * import {iconStyle} from '@react-spectrum/s2/style' with {type: 'macro'}; + * import Edit from '@react-spectrum/s2/icons/Edit'; + * + * + * ``` + */ export function iconStyle(this: MacroContext | void, options: IconStyle): StyleString> { let {size = 'M', color, ...styles} = options; diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index b66fa550564..0b6534ac895 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -194,6 +194,20 @@ class SpectrumColorProperty extends ArbitraryProperty { type BaseColor = keyof typeof baseColors; +/** + * Returns a set of stateful color token references for the default, hovered, focus-visible, + * and pressed states of a component. + * + * @param base - A Spectrum base color token name (e.g. `'gray-100'`, `'accent-900'`). + * @returns An object with `default`, `isHovered`, `isFocusVisible`, and `isPressed` color token references. + * + * @example + * ```tsx + * const styles = style({ + * backgroundColor: baseColor('gray-100') + * }); + * ``` + */ export function baseColor(base: BaseColor | C): {default: C, isHovered: C, isFocusVisible: C, isPressed: C} { return { default: base as C, @@ -204,6 +218,22 @@ export function baseColor(base: BaseColor | C): {d } type SpectrumColor = Color | ArbitraryValue; + +/** + * Resolves a Spectrum color token name to a CSS color value string. + * Supports opacity modifiers via the `color/opacity` syntax. + * + * @param value - A Spectrum color token (e.g. `'gray-800'`, `'accent-900/50'`) or an arbitrary CSS color value. + * @returns A CSS color string. + * + * @example + * ```tsx + * const styles = style({ + * color: color('gray-800'), + * borderColor: color('accent-900/50') + * }); + * ``` + */ export function color(value: SpectrumColor): string { let arbitrary = parseArbitraryValue(value); if (arbitrary) { @@ -213,10 +243,40 @@ export function color(value: SpectrumColor): string { return colorTokenToString(resolveColorToken(baseColors[colorValue]), opacity); } +/** + * Produces a `light-dark()` CSS color value that resolves to different colors + * depending on the current color scheme. + * + * @param light - The color to use in light mode. + * @param dark - The color to use in dark mode. + * @returns A CSS `light-dark()` expression wrapped as an arbitrary style value. + * + * @example + * ```tsx + * const styles = style({ + * backgroundColor: lightDark('gray-25', 'gray-900') + * }); + * ``` + */ export function lightDark(light: SpectrumColor, dark: SpectrumColor): `[${string}]` { return `[light-dark(${color(light)}, ${color(dark)})]`; } +/** + * Mixes two Spectrum colors by a given percentage using CSS `color-mix()` in sRGB color space. + * + * @param a - The first color. + * @param b - The second color. + * @param percent - The percentage of the second color in the mix (0–100). + * @returns A CSS `color-mix()` expression wrapped as an arbitrary style value. + * + * @example + * ```tsx + * const styles = style({ + * backgroundColor: colorMix('accent-900', 'gray-25', 50) + * }); + * ``` + */ export function colorMix(a: SpectrumColor, b: SpectrumColor, percent: number): `[${string}]` { return `[color-mix(in srgb, ${color(a)}, ${color(b)} ${percent}%)]`; } @@ -312,6 +372,19 @@ export type PositiveSpacing = keyof typeof baseSpacing; export type NegativeSpacing = keyof typeof negativeSpacing; export type Spacing = PositiveSpacing | NegativeSpacing; +/** + * Converts a pixel value to a font-relative `em` length. + * + * @param base - The pixel value to convert. + * @param baseFontSize - The base font size in pixels to divide by. Defaults to `14`. + * @returns A CSS `em` value string. + * + * @example + * ```tsx + * fontRelative(7) // => '0.5em' (7 / 14) + * fontRelative(8, 16) // => '0.5em' (8 / 16) + * ``` + */ export function fontRelative(this: MacroContext | void, base: number, baseFontSize = 14): string { return (base / baseFontSize) + 'em'; } @@ -320,6 +393,18 @@ export function edgeToText(this: MacroContext | void, height: keyof typeof baseS return `calc(${baseSpacing[height]} * 3 / 8)`; } +/** + * Converts a pixel value to `rem` units using a 16px base. + * + * @param px - The spacing in pixels. + * @returns A `rem` value string. + * + * @example + * ```tsx + * space(16) // => '1rem' + * space(8) // => '0.5rem' + * ``` + */ export function space(this: MacroContext | void, px: number): string { return pxToRem(px); } @@ -346,6 +431,21 @@ const padding = { ...relativeSpacing }; +/** + * Converts a pixel value to a scalable CSS size expression using the Spectrum 2 scale factor. + * The result is a `calc()` expression that multiplies the rem-converted value by the current scale factor. + * + * @param px - The size in pixels. + * @returns A CSS `calc()` expression. + * + * @example + * ```tsx + * const styles = style({ + * width: size(200), + * height: size(48) + * }); + * ``` + */ export function size(this: MacroContext | void, px: number): `calc(${string})` { return `calc(${pxToRem(px)} * var(--s2-scale))`; } diff --git a/packages/@react-spectrum/s2/style/style-macro.ts b/packages/@react-spectrum/s2/style/style-macro.ts index 512ac202f44..c7d3fe570c7 100644 --- a/packages/@react-spectrum/s2/style/style-macro.ts +++ b/packages/@react-spectrum/s2/style/style-macro.ts @@ -876,7 +876,7 @@ class ConditionalRule extends GroupRule { * ```tsx * import {raw} from '@react-spectrum/s2/style' with {type: 'macro'}; * - * const className = raw(` + * const styles = raw(` * backdrop-filter: blur(8px); * `); * ``` @@ -927,7 +927,7 @@ export function raw(this: MacroContext | void, css: string, layer = '_.a'): stri * to { opacity: 1; } * `); * - * const className = style({ + * const styles = style({ * animation: fadeIn, * }); * ``` From 68a080fec93b5e12aef94d82b7056ed64afe9acd Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 11:18:40 -0600 Subject: [PATCH 09/17] add docs --- packages/dev/s2-docs/pages/s2/style-macro.mdx | 169 ++++++++++++++++++ packages/dev/s2-docs/pages/s2/styling.mdx | 6 +- 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/packages/dev/s2-docs/pages/s2/style-macro.mdx b/packages/dev/s2-docs/pages/s2/style-macro.mdx index 0e23cc5968d..c7af7d0b458 100644 --- a/packages/dev/s2-docs/pages/s2/style-macro.mdx +++ b/packages/dev/s2-docs/pages/s2/style-macro.mdx @@ -55,3 +55,172 @@ Note that `font` should be applied on a per element basis rather than globally s ## Conditions + +## Utilities + +The style macro system provides built-in utility functions for common patterns. + +### baseColor + +Returns a set of stateful color token references for the default, hovered, focus-visible, and pressed states of a component. + +```tsx +const styles = style({ + backgroundColor: baseColor('gray-100') +}); +``` + +### color + +Resolves a Spectrum color token name to a CSS color value string. Supports opacity modifiers via the `color/opacity` syntax. + +```tsx +const styles = style({ + color: color('gray-800'), + borderColor: color('accent-900/50') +}); +``` + +### lightDark + +Produces a `light-dark()` CSS color value that resolves to different colors depending on the current color scheme. + +```tsx +const styles = style({ + backgroundColor: lightDark('gray-25', 'gray-900') +}); +``` + +### colorMix + +Mixes two Spectrum colors by a given percentage using CSS `color-mix()` in sRGB color space. + +```tsx +const styles = style({ + backgroundColor: colorMix('accent-900', 'gray-25', 50) +}); +``` + +### size + +Converts a pixel value to a scalable CSS size expression using the current Spectrum scale factor. + +```tsx +const styles = style({ + width: size(200), + height: size(48) +}); +``` + +### space + +Converts a pixel value to a Spectrum spacing token in `rem` units. + +```tsx +const styles = style({ + gap: space(12) +}); +``` + +### fontRelative + +Converts a pixel value to a font-relative `em` length. Useful for sizing elements relative to the current font size. + +```tsx +const styles = style({ + gap: fontRelative(2) // 2/14 ≈ 0.143em +}); +``` + +### focusRing + +Returns consistent Spectrum focus ring outline styles for interactive components. Requires `isFocusVisible` to be passed as a condition. + +```tsx +const styles = style({ + ...focusRing(), + borderRadius: 'lg' +}); +``` + +### iconStyle + +Generates styles for an icon element with the given size, color, and layout options. See the [Icons](icons#api) page for more information. + +```tsx + +``` + +### raw + +Injects a raw CSS string into the style system. The CSS is wrapped in a generated class name and placed within a CSS `@layer`. Returns the generated class name. + +```tsx +const blurClass = raw(` + backdrop-filter: blur(8px); +`); +``` + +### keyframes + +Defines a CSS `@keyframes` animation and returns the generated animation name. + +```tsx +const fadeIn = keyframes(` + from { opacity: 0; } + to { opacity: 1; } +`); + +const styles = style({ + animation: fadeIn +}); +``` + +### mergeStyles + +Merges multiple style strings together at runtime, combining the CSS properties from each. Later styles take precedence over earlier ones for the same property. + +```tsx +import {mergeStyles} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +const baseStyles = style({padding: 8}); +const overrideStyles = style({padding: 16, color: 'heading'}); +const merged = mergeStyles(baseStyles, overrideStyles); +``` + +### getAllowedOverrides + +Returns the list of CSS property names that are allowed as style overrides via the `styles` prop. By default includes layout properties (margin, position, grid, etc.) and width properties. Optionally includes height properties. + +```tsx +const styles = style({ + // ...component styles +}, getAllowedOverrides()); + +// With height overrides enabled: +const styles = style({ + // ...component styles +}, getAllowedOverrides({height: true})); +``` + +### centerPadding + +Calculates vertical padding to center a single line of text within a container using the CSS `self()` function and `1lh` unit. + +```tsx +const styles = style({ + paddingY: centerPadding() +}); +``` + +### setColorScheme + +Returns style properties that set the CSS `color-scheme` for a component. Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values via the `colorScheme` render prop condition. + +```tsx +const styles = style({ + ...setColorScheme(), + backgroundColor: 'layer-1' +}); +``` diff --git a/packages/dev/s2-docs/pages/s2/styling.mdx b/packages/dev/s2-docs/pages/s2/styling.mdx index f791f8fb97b..c2f168b00ce 100644 --- a/packages/dev/s2-docs/pages/s2/styling.mdx +++ b/packages/dev/s2-docs/pages/s2/styling.mdx @@ -238,12 +238,12 @@ const styles = style({ ### Built-in utilities -Use `focusRing()` to add the standard Spectrum focus ring. +The style macro system includes built-in utilities for common patterns like focus rings, color helpers, spacing, sizing, animations, and more. For example, use `focusRing()` to add the standard Spectrum focus ring to interactive components: ```tsx "use client"; import {style, focusRing} from '@react-spectrum/s2/style' with {type: 'macro'}; -import {Button} from '@react-spectrum/s2'; +import {Button} from 'react-aria-components'; const buttonStyle = style({ ...focusRing(), @@ -253,6 +253,8 @@ const buttonStyle = style({ ``` +See the [Utilities](style-macro#utilities) section in the style macro reference page for a full list of available utilities and examples. + ## Setting CSS variables CSS variables can be directly defined in a `style` macro, allowing child elements to then access them in their own styles. From ef9ab3198f74abde08fece68c3adb2eb5c94d759 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 11 Feb 2026 12:07:00 -0600 Subject: [PATCH 10/17] lint --- packages/@react-spectrum/s2/src/CoachMark.tsx | 2 +- packages/@react-spectrum/s2/src/Modal.tsx | 2 +- packages/@react-spectrum/s2/src/Popover.tsx | 2 +- packages/@react-spectrum/s2/src/Provider.tsx | 2 +- packages/@react-spectrum/s2/src/TableView.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@react-spectrum/s2/src/CoachMark.tsx b/packages/@react-spectrum/s2/src/CoachMark.tsx index def76c6a1a2..8bf076df815 100644 --- a/packages/@react-spectrum/s2/src/CoachMark.tsx +++ b/packages/@react-spectrum/s2/src/CoachMark.tsx @@ -27,7 +27,6 @@ import { import {ButtonContext} from './Button'; import {Card} from './Card'; import {CheckboxContext} from './Checkbox'; -import {setColorScheme, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {ColorSchemeContext} from './Provider'; import {ContentContext, FooterContext, KeyboardContext, TextContext} from './Content'; import { @@ -40,6 +39,7 @@ import { } from 'react'; import {DividerContext} from './Divider'; import {forwardRefType} from './types'; +import {getAllowedOverrides, setColorScheme, StyleProps} from './style-utils' with {type: 'macro'}; import {GlobalDOMAttributes} from '@react-types/shared'; import {ImageContext} from './Image'; import {ImageCoordinator} from './ImageCoordinator'; diff --git a/packages/@react-spectrum/s2/src/Modal.tsx b/packages/@react-spectrum/s2/src/Modal.tsx index fc9e1c6bbd6..b41f7e847f9 100644 --- a/packages/@react-spectrum/s2/src/Modal.tsx +++ b/packages/@react-spectrum/s2/src/Modal.tsx @@ -10,11 +10,11 @@ * governing permissions and limitations under the License. */ -import {setColorScheme} from './style-utils' with {type: 'macro'}; import {ColorSchemeContext} from './Provider'; import {DOMRef, GlobalDOMAttributes} from '@react-types/shared'; import {forwardRef, MutableRefObject, useCallback, useContext} from 'react'; import {ModalOverlay, ModalOverlayProps, Modal as RACModal, useLocale} from 'react-aria-components'; +import {setColorScheme} from './style-utils' with {type: 'macro'}; import {style} from '../style' with {type: 'macro'}; import {useDOMRef} from '@react-spectrum/utils'; diff --git a/packages/@react-spectrum/s2/src/Popover.tsx b/packages/@react-spectrum/s2/src/Popover.tsx index 6da461a7b0a..79aa5900aa9 100644 --- a/packages/@react-spectrum/s2/src/Popover.tsx +++ b/packages/@react-spectrum/s2/src/Popover.tsx @@ -20,10 +20,10 @@ import { OverlayTriggerStateContext, useLocale } from 'react-aria-components'; -import {setColorScheme, getAllowedOverrides, heightProperties, UnsafeStyles, widthProperties} from './style-utils' with {type: 'macro'}; import {ColorSchemeContext} from './Provider'; import {createContext, ForwardedRef, forwardRef, ReactNode, useCallback, useContext, useMemo} from 'react'; import {DOMRef, DOMRefValue, GlobalDOMAttributes} from '@react-types/shared'; +import {getAllowedOverrides, heightProperties, setColorScheme, UnsafeStyles, widthProperties} from './style-utils' with {type: 'macro'}; import {lightDark, style} from '../style' with {type: 'macro'}; import {mergeRefs} from '@react-aria/utils'; import {mergeStyles} from '../style/runtime'; diff --git a/packages/@react-spectrum/s2/src/Provider.tsx b/packages/@react-spectrum/s2/src/Provider.tsx index 314f4e207e0..5b1551d4fd9 100644 --- a/packages/@react-spectrum/s2/src/Provider.tsx +++ b/packages/@react-spectrum/s2/src/Provider.tsx @@ -11,7 +11,6 @@ */ import type {ColorScheme, Router} from '@react-types/provider'; -import {setColorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {createContext, JSX, ReactNode, useContext} from 'react'; import {DOMProps} from '@react-types/shared'; import {filterDOMProps} from '@react-aria/utils'; @@ -19,6 +18,7 @@ import {Fonts} from './Fonts'; import {generateDefaultColorSchemeStyles} from './page.macro' with {type: 'macro'}; import {I18nProvider, RouterProvider, useLocale} from 'react-aria-components'; import {mergeStyles} from '../style/runtime'; +import {setColorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {style} from '../style' with {type: 'macro'}; import {StyleString} from '../style/types'; diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index ff8b42cca7e..4fa7e6b8719 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -53,7 +53,7 @@ import { Virtualizer } from 'react-aria-components'; import {ButtonGroup} from './ButtonGroup'; -import {centerPadding, setColorScheme, controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; +import {centerPadding, controlFont, getAllowedOverrides, setColorScheme, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {Checkbox} from './Checkbox'; import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg'; import Chevron from '../ui-icons/Chevron'; From c8be4729900e55ade9d1e52769a54300064012a8 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 10:39:40 -0600 Subject: [PATCH 11/17] address review comments --- .../@react-spectrum/s2/src/style-utils.ts | 6 ++- packages/@react-spectrum/s2/style/index.ts | 6 +-- packages/@react-spectrum/s2/style/runtime.ts | 1 + .../s2/style/spectrum-theme.ts | 26 +------------ .../@react-spectrum/s2/style/style-macro.ts | 2 + packages/dev/s2-docs/pages/s2/style-macro.mdx | 37 +++++++++++++++++-- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index 4de89803f10..200df3d47b0 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -18,6 +18,7 @@ import {StyleString} from '../style/types'; * Calculates vertical padding to center a single line of text within a container. * Uses the CSS `self()` function and `1lh` unit to compute the padding based on * the container's minimum height and border widths. + * This is useful for precise vertical centering without introducing a flex/grid layout to the container. * * @param minHeight - A CSS expression for the minimum height to center within. Defaults to `'self(minHeight)'`. * @returns A CSS `calc()` expression wrapped as an arbitrary style value. @@ -132,6 +133,7 @@ export const fieldInput = () => ({ * Returns style properties that set the CSS `color-scheme` for a component. * Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values * via the `colorScheme` render prop condition. + * Intended for root containers (e.g. providers, modals, and popovers), and not needed for individual components. * * @example * ```tsx @@ -350,7 +352,7 @@ export const heightProperties = [ 'maxHeight' ] as const; -/** The set of height-related CSS property names (`size`, `height`, `minHeight`, `maxHeight`). */ +/** The set of height-related CSS property names (`height`, `minHeight`, `maxHeight`) plus `size` (which controls both width and height). */ export type HeightProperties = (typeof heightProperties)[number]; export type StylesProp = StyleString<(typeof allowedOverrides)[number] | (typeof widthProperties)[number]>; @@ -376,7 +378,7 @@ export interface StyleProps extends UnsafeStyles { * * @param options - Configuration for which property groups to include. * @param options.width - Whether to include width properties (`width`, `minWidth`, `maxWidth`). Defaults to `true`. - * @param options.height - Whether to include height properties (`height`, `minHeight`, `maxHeight`, `size`). Defaults to `false`. + * @param options.height - Whether to include height properties (`height`, `minHeight`, `maxHeight`) and `size` (applies to both axes). Defaults to `false`. * @returns An array of allowed CSS property names. * * @example diff --git a/packages/@react-spectrum/s2/style/index.ts b/packages/@react-spectrum/s2/style/index.ts index 6fb6291c9ec..787dc27dded 100644 --- a/packages/@react-spectrum/s2/style/index.ts +++ b/packages/@react-spectrum/s2/style/index.ts @@ -30,7 +30,7 @@ export type {StyleString} from './types'; * import {space} from '@react-spectrum/s2/style' with {type: 'macro'}; * * const styles = style({ - * gap: space(12) + * gap: space(12) // 12/16 = 0.75rem * }); * ``` */ @@ -40,7 +40,7 @@ export function space(px: number): `[${string}]` { /** * Converts a pixel value to a font-relative `em` length. Useful for sizing elements - * relative to the current font size. + * relative to the current font size. Defaults to a 14px base. * * @param base - The pixel value to convert. * @param baseFontSize - The base font size in pixels to divide by. Defaults to `14`. @@ -55,7 +55,7 @@ export function space(px: number): `[${string}]` { * }); * ``` */ -export function fontRelative(base: number, baseFontSize?: number): `[${string}]` { +export function fontRelative(base: number, baseFontSize = 14): `[${string}]` { return `[${internalFontRelative(base, baseFontSize)}]`; } diff --git a/packages/@react-spectrum/s2/style/runtime.ts b/packages/@react-spectrum/s2/style/runtime.ts index 96d375c8f5b..752b5858728 100644 --- a/packages/@react-spectrum/s2/style/runtime.ts +++ b/packages/@react-spectrum/s2/style/runtime.ts @@ -49,6 +49,7 @@ import {StyleString} from './types'; * const baseStyles = style({padding: 8}); * const overrideStyles = style({padding: 16, color: 'heading'}); * const merged = mergeStyles(baseStyles, overrideStyles); + * // merged has `padding: 16` and `color: heading`. * ``` */ export function mergeStyles(...styles: (StyleString | null | undefined)[]): StyleString { diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index 0b6534ac895..0aaa370931b 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -372,19 +372,6 @@ export type PositiveSpacing = keyof typeof baseSpacing; export type NegativeSpacing = keyof typeof negativeSpacing; export type Spacing = PositiveSpacing | NegativeSpacing; -/** - * Converts a pixel value to a font-relative `em` length. - * - * @param base - The pixel value to convert. - * @param baseFontSize - The base font size in pixels to divide by. Defaults to `14`. - * @returns A CSS `em` value string. - * - * @example - * ```tsx - * fontRelative(7) // => '0.5em' (7 / 14) - * fontRelative(8, 16) // => '0.5em' (8 / 16) - * ``` - */ export function fontRelative(this: MacroContext | void, base: number, baseFontSize = 14): string { return (base / baseFontSize) + 'em'; } @@ -393,18 +380,6 @@ export function edgeToText(this: MacroContext | void, height: keyof typeof baseS return `calc(${baseSpacing[height]} * 3 / 8)`; } -/** - * Converts a pixel value to `rem` units using a 16px base. - * - * @param px - The spacing in pixels. - * @returns A `rem` value string. - * - * @example - * ```tsx - * space(16) // => '1rem' - * space(8) // => '0.5rem' - * ``` - */ export function space(this: MacroContext | void, px: number): string { return pxToRem(px); } @@ -434,6 +409,7 @@ const padding = { /** * Converts a pixel value to a scalable CSS size expression using the Spectrum 2 scale factor. * The result is a `calc()` expression that multiplies the rem-converted value by the current scale factor. + * The scale factor differs between touch and non-touch devices. * * @param px - The size in pixels. * @returns A CSS `calc()` expression. diff --git a/packages/@react-spectrum/s2/style/style-macro.ts b/packages/@react-spectrum/s2/style/style-macro.ts index c7d3fe570c7..18b870ba6f0 100644 --- a/packages/@react-spectrum/s2/style/style-macro.ts +++ b/packages/@react-spectrum/s2/style/style-macro.ts @@ -866,6 +866,8 @@ class ConditionalRule extends GroupRule { /** * Injects a raw CSS string into the style system. The CSS is wrapped in a generated * class name and placed within the specified `@layer`. Returns the generated class name. + * This is an escape hatch for advanced cases (e.g. pseudo selectors or features not yet + * available in the style macro API), and should be used sparingly. * Must be imported with `{type: 'macro'}`. * * @param css - The raw CSS declarations to inject. diff --git a/packages/dev/s2-docs/pages/s2/style-macro.mdx b/packages/dev/s2-docs/pages/s2/style-macro.mdx index c7af7d0b458..db87b95eb80 100644 --- a/packages/dev/s2-docs/pages/s2/style-macro.mdx +++ b/packages/dev/s2-docs/pages/s2/style-macro.mdx @@ -65,6 +65,8 @@ The style macro system provides built-in utility functions for common patterns. Returns a set of stateful color token references for the default, hovered, focus-visible, and pressed states of a component. ```tsx +import {baseColor, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ backgroundColor: baseColor('gray-100') }); @@ -75,6 +77,8 @@ const styles = style({ Resolves a Spectrum color token name to a CSS color value string. Supports opacity modifiers via the `color/opacity` syntax. ```tsx +import {color, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ color: color('gray-800'), borderColor: color('accent-900/50') @@ -86,6 +90,8 @@ const styles = style({ Produces a `light-dark()` CSS color value that resolves to different colors depending on the current color scheme. ```tsx +import {lightDark, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ backgroundColor: lightDark('gray-25', 'gray-900') }); @@ -96,6 +102,8 @@ const styles = style({ Mixes two Spectrum colors by a given percentage using CSS `color-mix()` in sRGB color space. ```tsx +import {colorMix, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ backgroundColor: colorMix('accent-900', 'gray-25', 50) }); @@ -106,6 +114,8 @@ const styles = style({ Converts a pixel value to a scalable CSS size expression using the current Spectrum scale factor. ```tsx +import {size, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ width: size(200), height: size(48) @@ -117,6 +127,8 @@ const styles = style({ Converts a pixel value to a Spectrum spacing token in `rem` units. ```tsx +import {space, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ gap: space(12) }); @@ -127,8 +139,10 @@ const styles = style({ Converts a pixel value to a font-relative `em` length. Useful for sizing elements relative to the current font size. ```tsx +import {fontRelative, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ - gap: fontRelative(2) // 2/14 ≈ 0.143em + gap: fontRelative(2) }); ``` @@ -137,6 +151,8 @@ const styles = style({ Returns consistent Spectrum focus ring outline styles for interactive components. Requires `isFocusVisible` to be passed as a condition. ```tsx +import {focusRing, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ ...focusRing(), borderRadius: 'lg' @@ -148,6 +164,8 @@ const styles = style({ Generates styles for an icon element with the given size, color, and layout options. See the [Icons](icons#api) page for more information. ```tsx +import {iconStyle} from '@react-spectrum/s2/style' with {type: 'macro'}; + ``` @@ -156,6 +174,8 @@ Generates styles for an icon element with the given size, color, and layout opti Injects a raw CSS string into the style system. The CSS is wrapped in a generated class name and placed within a CSS `@layer`. Returns the generated class name. ```tsx +import {raw} from '@react-spectrum/s2/style' with {type: 'macro'}; + const blurClass = raw(` backdrop-filter: blur(8px); `); @@ -166,6 +186,8 @@ const blurClass = raw(` Defines a CSS `@keyframes` animation and returns the generated animation name. ```tsx +import {keyframes, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const fadeIn = keyframes(` from { opacity: 0; } to { opacity: 1; } @@ -194,6 +216,9 @@ const merged = mergeStyles(baseStyles, overrideStyles); Returns the list of CSS property names that are allowed as style overrides via the `styles` prop. By default includes layout properties (margin, position, grid, etc.) and width properties. Optionally includes height properties. ```tsx +import {getAllowedOverrides} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ // ...component styles }, getAllowedOverrides()); @@ -206,9 +231,12 @@ const styles = style({ ### centerPadding -Calculates vertical padding to center a single line of text within a container using the CSS `self()` function and `1lh` unit. +Calculates vertical padding to center a single line of text within a container using the CSS `self()` function and `1lh` unit. Useful when flex centering is not practical for a given layout. ```tsx +import {centerPadding} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ paddingY: centerPadding() }); @@ -216,9 +244,12 @@ const styles = style({ ### setColorScheme -Returns style properties that set the CSS `color-scheme` for a component. Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values via the `colorScheme` render prop condition. +Returns style properties that set the CSS `color-scheme` for a component. Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values via the `colorScheme` render prop condition. Intended for container surfaces like providers, modals, and popovers. ```tsx +import {setColorScheme} from '@react-spectrum/s2'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + const styles = style({ ...setColorScheme(), backgroundColor: 'layer-1' From efcd314e8549a58a24cfb9bb2b6698f2d4daa49e Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 11:36:56 -0600 Subject: [PATCH 12/17] extract docs from JSDoc --- .../dev/parcel-packager-docs/DocsPackager.js | 1 + .../DocsTransformer.js | 67 ++++++ packages/dev/s2-docs/pages/s2/style-macro.mdx | 170 ++------------- .../s2-docs/scripts/generateMarkdownDocs.mjs | 199 ++++++++++++++++++ packages/dev/s2-docs/src/FunctionJSDoc.tsx | 69 ++++++ 5 files changed, 355 insertions(+), 151 deletions(-) create mode 100644 packages/dev/s2-docs/src/FunctionJSDoc.tsx diff --git a/packages/dev/parcel-packager-docs/DocsPackager.js b/packages/dev/parcel-packager-docs/DocsPackager.js index 8f243bd0ef7..8c3d9ebdfcd 100644 --- a/packages/dev/parcel-packager-docs/DocsPackager.js +++ b/packages/dev/parcel-packager-docs/DocsPackager.js @@ -369,6 +369,7 @@ function visitChildren(obj, fn) { return: fn(obj.return, 'return'), typeParameters: obj.typeParameters.map(i => fn(i, 'typeParameters')), description: obj.description, + examples: obj.examples, access: obj.access }; case 'interface': diff --git a/packages/dev/parcel-transformer-docs/DocsTransformer.js b/packages/dev/parcel-transformer-docs/DocsTransformer.js index 5cc8cca9b2b..1797cd8f5fe 100644 --- a/packages/dev/parcel-transformer-docs/DocsTransformer.js +++ b/packages/dev/parcel-transformer-docs/DocsTransformer.js @@ -767,6 +767,10 @@ module.exports = new Transformer({ let result = { description: parsed.description }; + let extractedExamples = extractExamples(comments); + if (extractedExamples.length > 0) { + result.examples = extractedExamples; + } for (let tag of parsed.tags) { if (tag.title === 'default') { @@ -789,15 +793,74 @@ module.exports = new Transformer({ result.params[tag.name] = tag.description; } else if (tag.title === 'selector') { result.selector = tag.description; + } else if (tag.title === 'example') { + if (!result.examples) { + result.examples = []; + } + + if (tag.description) { + result.examples.push(tag.description); + } } } + if (result.examples) { + result.examples = [...new Set(result.examples.map(example => example.trim()).filter(Boolean))]; + } + return result; } return {}; } + function extractExamples(comments) { + let lines = comments.split('\n') + .map(line => line.replace(/^\s*\*?\s?/, '')); + let examples = []; + let current = null; + + for (let line of lines) { + if (/^@example\b/.test(line)) { + if (current) { + let prev = current.join('\n').trim(); + if (prev) { + examples.push(prev); + } + } + + current = []; + let inlineExample = line.replace(/^@example\b\s*/, ''); + if (inlineExample) { + current.push(inlineExample); + } + continue; + } + + if (current) { + if (/^@\w+/.test(line)) { + let example = current.join('\n').trim(); + if (example) { + examples.push(example); + } + current = null; + continue; + } + + current.push(line); + } + } + + if (current) { + let example = current.join('\n').trim(); + if (example) { + examples.push(example); + } + } + + return examples; + } + function getDocComments(path) { if (path.node.leadingComments) { return path.node.leadingComments.filter(isJSDocComment).map(c => c.value).join('\n'); @@ -857,6 +920,10 @@ module.exports = new Transformer({ if (value.return) { value.return.description = docs.return || value.return.description || null; } + + if (docs.examples) { + value.examples = docs.examples; + } } asset.type = 'json'; diff --git a/packages/dev/s2-docs/pages/s2/style-macro.mdx b/packages/dev/s2-docs/pages/s2/style-macro.mdx index db87b95eb80..8e7b3bbd466 100644 --- a/packages/dev/s2-docs/pages/s2/style-macro.mdx +++ b/packages/dev/s2-docs/pages/s2/style-macro.mdx @@ -1,6 +1,9 @@ import {Layout} from '../../src/Layout'; import {InlineAlert, Heading, Content, Link} from '@react-spectrum/s2'; +import {FunctionJSDoc} from '../../src/FunctionJSDoc'; import {StyleMacroProperties} from '../../src/StyleMacroProperties'; +import docs from 'docs:@react-spectrum/s2'; +import styleDocs from 'docs:@react-spectrum/s2/style'; import {getPropertyDefinitions} from '../../src/styleProperties'; export default Layout; @@ -62,196 +65,61 @@ The style macro system provides built-in utility functions for common patterns. ### baseColor -Returns a set of stateful color token references for the default, hovered, focus-visible, and pressed states of a component. - -```tsx -import {baseColor, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - backgroundColor: baseColor('gray-100') -}); -``` + ### color -Resolves a Spectrum color token name to a CSS color value string. Supports opacity modifiers via the `color/opacity` syntax. - -```tsx -import {color, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - color: color('gray-800'), - borderColor: color('accent-900/50') -}); -``` + ### lightDark -Produces a `light-dark()` CSS color value that resolves to different colors depending on the current color scheme. - -```tsx -import {lightDark, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - backgroundColor: lightDark('gray-25', 'gray-900') -}); -``` + ### colorMix -Mixes two Spectrum colors by a given percentage using CSS `color-mix()` in sRGB color space. - -```tsx -import {colorMix, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - backgroundColor: colorMix('accent-900', 'gray-25', 50) -}); -``` + ### size -Converts a pixel value to a scalable CSS size expression using the current Spectrum scale factor. - -```tsx -import {size, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - width: size(200), - height: size(48) -}); -``` + ### space -Converts a pixel value to a Spectrum spacing token in `rem` units. - -```tsx -import {space, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - gap: space(12) -}); -``` + ### fontRelative -Converts a pixel value to a font-relative `em` length. Useful for sizing elements relative to the current font size. - -```tsx -import {fontRelative, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - gap: fontRelative(2) -}); -``` + ### focusRing -Returns consistent Spectrum focus ring outline styles for interactive components. Requires `isFocusVisible` to be passed as a condition. - -```tsx -import {focusRing, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - ...focusRing(), - borderRadius: 'lg' -}); -``` + ### iconStyle -Generates styles for an icon element with the given size, color, and layout options. See the [Icons](icons#api) page for more information. - -```tsx -import {iconStyle} from '@react-spectrum/s2/style' with {type: 'macro'}; - - -``` + +See the [Icons](icons#api) page for more information. ### raw -Injects a raw CSS string into the style system. The CSS is wrapped in a generated class name and placed within a CSS `@layer`. Returns the generated class name. - -```tsx -import {raw} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const blurClass = raw(` - backdrop-filter: blur(8px); -`); -``` + ### keyframes -Defines a CSS `@keyframes` animation and returns the generated animation name. - -```tsx -import {keyframes, style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const fadeIn = keyframes(` - from { opacity: 0; } - to { opacity: 1; } -`); - -const styles = style({ - animation: fadeIn -}); -``` + ### mergeStyles -Merges multiple style strings together at runtime, combining the CSS properties from each. Later styles take precedence over earlier ones for the same property. - -```tsx -import {mergeStyles} from '@react-spectrum/s2'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const baseStyles = style({padding: 8}); -const overrideStyles = style({padding: 16, color: 'heading'}); -const merged = mergeStyles(baseStyles, overrideStyles); -``` + ### getAllowedOverrides -Returns the list of CSS property names that are allowed as style overrides via the `styles` prop. By default includes layout properties (margin, position, grid, etc.) and width properties. Optionally includes height properties. - -```tsx -import {getAllowedOverrides} from '@react-spectrum/s2'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - // ...component styles -}, getAllowedOverrides()); - -// With height overrides enabled: -const styles = style({ - // ...component styles -}, getAllowedOverrides({height: true})); -``` + ### centerPadding -Calculates vertical padding to center a single line of text within a container using the CSS `self()` function and `1lh` unit. Useful when flex centering is not practical for a given layout. - -```tsx -import {centerPadding} from '@react-spectrum/s2'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - paddingY: centerPadding() -}); -``` + ### setColorScheme -Returns style properties that set the CSS `color-scheme` for a component. Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values via the `colorScheme` render prop condition. Intended for container surfaces like providers, modals, and popovers. - -```tsx -import {setColorScheme} from '@react-spectrum/s2'; -import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - -const styles = style({ - ...setColorScheme(), - backgroundColor: 'layer-1' -}); -``` + diff --git a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs index 73b2e7aee08..e5c48736ca9 100644 --- a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs +++ b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs @@ -65,6 +65,7 @@ const interfaceTableCache = new Map(); const classTableCache = new Map(); const propTableCache = new Map(); const descriptionCache = new Map(); +const functionExamplesCache = new Map(); let tsFileIndex = null; let styleMacroDataCache = null; const styleMacroTableCache = new Map(); @@ -1049,6 +1050,154 @@ function getComponentDescription(componentName, file) { return null; } +/** + * Extracts one or more `@example` tag contents from JSDoc comments. + * @param {string} text - The text to extract examples from. + * @returns {string[]} An array of examples. + */ +function extractExamplesFromText(text) { + if (!text || typeof text !== 'string') { + return []; + } + + let lines = text.split('\n').map(line => line.replace(/^\s*\*?\s?/, '')); + let examples = []; + let current = null; + + for (let line of lines) { + if (/^@example\b/.test(line)) { + if (current) { + let prev = current.join('\n').trim(); + if (prev) { + examples.push(prev); + } + } + + current = []; + let inlineExample = line.replace(/^@example\b\s*/, ''); + if (inlineExample) { + current.push(inlineExample); + } + continue; + } + + if (current) { + if (/^@\w+/.test(line)) { + let example = current.join('\n').trim(); + if (example) { + examples.push(example); + } + current = null; + continue; + } + + current.push(line); + } + } + + if (current) { + let example = current.join('\n').trim(); + if (example) { + examples.push(example); + } + } + + return examples; +} + +function parseFencedCodeBlock(example) { + if (!example || typeof example !== 'string') { + return null; + } + + let trimmed = example.trim(); + let match = trimmed.match(/^```([^\n`]*)\n([\s\S]*?)\n```$/); + if (!match) { + return null; + } + + let [, lang, code] = match; + return { + lang: lang?.trim() || undefined, + code + }; +} + +function getFunctionExamples(functionName, file) { + const cacheKey = getCacheKey(`${functionName}:examples`, file); + if (functionExamplesCache.has(cacheKey)) { + return functionExamplesCache.get(cacheKey); + } + + const functionPath = resolveComponentPath(functionName, file); + if (!functionPath) { + functionExamplesCache.set(cacheKey, []); + return []; + } + + const source = project.addSourceFileAtPathIfExists(functionPath); + if (!source) { + functionExamplesCache.set(cacheKey, []); + return []; + } + + const exportedDecl = source.getExportedDeclarations().get(functionName)?.[0]; + const possibleNodes = [exportedDecl, source.getVariableDeclaration(functionName), source.getFunction(functionName)]; + + let firstNodeExamples = []; + for (let node of possibleNodes.filter(Boolean)) { + let current = node; + let isDirectNode = true; + + while (current) { + let docs = typeof current.getJsDocs === 'function' ? current.getJsDocs() : []; + if (!docs?.length) { + isDirectNode = false; + current = current.getParent?.(); + continue; + } + + let directExamples = []; + for (let doc of docs) { + let tags = doc.getTags?.() || []; + let tagExamples = tags + .filter(tag => tag.getTagName?.() === 'example') + .map(tag => tag.getCommentText?.()) + .filter(Boolean) + .map(value => value.trim()); + directExamples.push(...tagExamples); + + if (tagExamples.length === 0) { + let docText = doc.getInnerText?.() || doc.getText?.() || ''; + directExamples.push(...extractExamplesFromText(docText)); + } + } + + directExamples = [...new Set(directExamples.filter(Boolean))]; + if (!directExamples.length) { + isDirectNode = false; + current = current.getParent?.(); + continue; + } + + if (isDirectNode) { + functionExamplesCache.set(cacheKey, directExamples); + return directExamples; + } + + if (!firstNodeExamples.length) { + firstNodeExamples = directExamples; + } + + isDirectNode = false; + current = current.getParent?.(); + } + } + + functionExamplesCache.set(cacheKey, firstNodeExamples); + return firstNodeExamples; +} + /** * Build a markdown table of props for the given component by analyzing its interface. */ @@ -1502,6 +1651,56 @@ function remarkDocsComponentsToMarkdown() { return index; } + // Render function description + examples from JSDoc. + if (name === 'FunctionJSDoc') { + const functionAttr = node.attributes?.find((a) => a.name === 'function'); + let functionName = null; + if (functionAttr && functionAttr.value?.type === 'mdxJsxAttributeValueExpression') { + const m = functionAttr.value.value.match(/\.exports\.([\w$]+)/); + if (m) { + functionName = m[1]; + } + } + + if (!functionName) { + parent.children.splice(index, 1); + return index; + } + + const newNodes = []; + const description = getComponentDescription(functionName, file); + if (description) { + const descTree = unified().use(remarkParse).parse(description); + newNodes.push(...descTree.children); + } + + const examples = getFunctionExamples(functionName, file); + for (let [exampleIndex, example] of examples.entries()) { + if (examples.length > 1) { + newNodes.push({ + type: 'paragraph', + children: [{type: 'strong', children: [{type: 'text', value: `Example ${exampleIndex + 1}:`}]}] + }); + } + + const fenced = parseFencedCodeBlock(example); + if (fenced) { + newNodes.push({ + type: 'code', + lang: fenced.lang || 'tsx', + meta: '', + value: fenced.code + }); + } else { + const exampleTree = unified().use(remarkParse).parse(example); + newNodes.push(...exampleTree.children); + } + } + + parent.children.splice(index, 1, ...newNodes); + return index + newNodes.length; + } + // Render a table of props. if (name === 'PropTable') { const compAttr = node.attributes?.find((a) => a.name === 'component'); diff --git a/packages/dev/s2-docs/src/FunctionJSDoc.tsx b/packages/dev/s2-docs/src/FunctionJSDoc.tsx new file mode 100644 index 00000000000..771b0cd56cc --- /dev/null +++ b/packages/dev/s2-docs/src/FunctionJSDoc.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2026 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 {Code} from './Code'; +import React from 'react'; +import {renderHTMLfromMarkdown} from './types'; +import {standaloneCode} from './CodeBlock'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +interface FunctionJSDocProps { + function: { + description?: string | null, + examples?: string[] + } +} + +function parseFencedCodeBlock(example: string): {lang?: string, code: string} | null { + let trimmed = example.trim(); + let match = trimmed.match(/^```([^\n`]*)\n([\s\S]*?)\n```$/); + if (!match) { + return null; + } + + let [, lang, code] = match; + return { + lang: lang?.trim() || undefined, + code + }; +} + +export function FunctionJSDoc({function: func}: FunctionJSDocProps) { + let examples = Array.isArray(func.examples) + ? func.examples.filter(Boolean) + : []; + + return ( +
+ {renderHTMLfromMarkdown(func.description, {forceInline: false, forceBlock: true})} + {examples.map((example, index) => { + let parsedExample = parseFencedCodeBlock(example); + return ( +
+ {examples.length > 1 && + + Example {index + 1}: + + } + {parsedExample + ? ( +
+                  {parsedExample.code}
+                
+ ) + : renderHTMLfromMarkdown(example, {forceInline: false, forceBlock: true})} +
+ ); + })} +
+ ); +} From 3003c94b918db388b035aa2de7ed4ee1b587faac Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 24 Feb 2026 11:37:27 -0600 Subject: [PATCH 13/17] add imports to all examples --- packages/@react-spectrum/s2/src/style-utils.ts | 9 +++++++++ packages/@react-spectrum/s2/style/index.ts | 2 ++ packages/@react-spectrum/s2/style/spectrum-theme.ts | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index 200df3d47b0..33af1c5da22 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -25,6 +25,9 @@ import {StyleString} from '../style/types'; * * @example * ```tsx + * import {centerPadding} from '@react-spectrum/s2'; + * import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * paddingY: centerPadding() * }); @@ -137,6 +140,9 @@ export const fieldInput = () => ({ * * @example * ```tsx + * import {setColorScheme} from '@react-spectrum/s2'; + * import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * ...setColorScheme(), * backgroundColor: 'layer-1' @@ -383,6 +389,9 @@ export interface StyleProps extends UnsafeStyles { * * @example * ```tsx + * import {getAllowedOverrides} from '@react-spectrum/s2'; + * import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * // ... component styles * }, getAllowedOverrides()); diff --git a/packages/@react-spectrum/s2/style/index.ts b/packages/@react-spectrum/s2/style/index.ts index 787dc27dded..903adbd9a7b 100644 --- a/packages/@react-spectrum/s2/style/index.ts +++ b/packages/@react-spectrum/s2/style/index.ts @@ -64,6 +64,8 @@ export function fontRelative(base: number, baseFontSize = 14): `[${string}]` { * * @example * ```tsx + * import {focusRing, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * ...focusRing(), * borderRadius: 'lg' diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index 0aaa370931b..0cf5302c724 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -203,6 +203,8 @@ type BaseColor = keyof typeof baseColors; * * @example * ```tsx + * import {baseColor, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * backgroundColor: baseColor('gray-100') * }); @@ -228,6 +230,8 @@ type SpectrumColor = Color | ArbitraryValue; * * @example * ```tsx + * import {color, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * color: color('gray-800'), * borderColor: color('accent-900/50') @@ -253,6 +257,8 @@ export function color(value: SpectrumColor): string { * * @example * ```tsx + * import {lightDark, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * backgroundColor: lightDark('gray-25', 'gray-900') * }); @@ -272,6 +278,8 @@ export function lightDark(light: SpectrumColor, dark: SpectrumColor): `[${string * * @example * ```tsx + * import {colorMix, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * backgroundColor: colorMix('accent-900', 'gray-25', 50) * }); @@ -416,6 +424,8 @@ const padding = { * * @example * ```tsx + * import {size, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * * const styles = style({ * width: size(200), * height: size(48) From c6e6cde9377b99c42f26fab1e87b3820654b9feb Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 9 Mar 2026 10:03:59 -0500 Subject: [PATCH 14/17] update styles->className in example --- packages/dev/s2-docs/pages/s2/styling.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev/s2-docs/pages/s2/styling.mdx b/packages/dev/s2-docs/pages/s2/styling.mdx index c2f168b00ce..3b2f2bd39bc 100644 --- a/packages/dev/s2-docs/pages/s2/styling.mdx +++ b/packages/dev/s2-docs/pages/s2/styling.mdx @@ -250,7 +250,7 @@ const buttonStyle = style({ // ...other styles }); - + ``` See the [Utilities](style-macro#utilities) section in the style macro reference page for a full list of available utilities and examples. From 15afa36c73a874e009b55789f84de3402d6bb2c9 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 9 Mar 2026 10:06:31 -0500 Subject: [PATCH 15/17] fix size in md output --- .../s2-docs/scripts/generateMarkdownDocs.mjs | 148 +++++++++++++++--- 1 file changed, 123 insertions(+), 25 deletions(-) diff --git a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs index e5c48736ca9..9bb543878a1 100644 --- a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs +++ b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs @@ -42,6 +42,7 @@ function getBaseUrl(library) { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.resolve(__dirname, '../../../../'); const S2_SRC_ROOT = path.join(REPO_ROOT, 'packages/@react-spectrum/s2/src'); +const S2_STYLE_ROOT = path.join(REPO_ROOT, 'packages/@react-spectrum/s2/style'); const RAC_SRC_ROOT = path.join(REPO_ROOT, 'packages/react-aria-components/src'); const INTL_SRC_ROOT = path.join(REPO_ROOT, 'packages/@internationalized'); const COMPONENT_SRC_ROOTS = [S2_SRC_ROOT, RAC_SRC_ROOT, INTL_SRC_ROOT]; @@ -886,38 +887,101 @@ function extractJSXText(node, file) { return ''; } -function getRootsForFile(file) { +function getCacheKey(name, file) { if (file?.path) { if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - return [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; + return `intl:${name}`; } else if (file.path.includes(path.join('pages', 'react-aria'))) { - return [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; + return `rac:${name}`; + } else if (file.path.includes(path.join('pages', 's2'))) { + return `s2:${name}`; } } - return COMPONENT_SRC_ROOTS; + return `default:${name}`; } -function getCacheKey(name, file) { +function getDocsImportSource(identifier, file) { + return file?.data?.docsImports?.[identifier] || null; +} + +function getExistingRoots(roots) { + return [...new Set(roots.filter(root => root && fs.existsSync(root)))]; +} + +function getRootsForDocsSource(docsSource, file) { + if (!docsSource) { + return null; + } + + if (docsSource === '@react-spectrum/s2') { + return getExistingRoots([S2_SRC_ROOT, S2_STYLE_ROOT]); + } + + if (docsSource === '@react-spectrum/s2/style') { + return getExistingRoots([S2_STYLE_ROOT]); + } + + if (docsSource.startsWith('./') || docsSource.startsWith('../')) { + if (!file?.path) { + return null; + } + + const resolved = path.resolve(path.dirname(file.path), docsSource); + const candidates = [ + resolved, + `${resolved}.ts`, + `${resolved}.tsx`, + `${resolved}.d.ts`, + path.join(resolved, 'index.ts'), + path.join(resolved, 'index.tsx'), + path.join(resolved, 'index.d.ts') + ]; + + return getExistingRoots(candidates.map(candidate => { + if (!fs.existsSync(candidate)) { + return null; + } + + return fs.statSync(candidate).isDirectory() ? candidate : path.dirname(candidate); + })); + } + + const packagePath = path.join(REPO_ROOT, 'packages', docsSource); + if (fs.existsSync(packagePath)) { + if (fs.statSync(packagePath).isDirectory()) { + return getExistingRoots([path.join(packagePath, 'src'), packagePath]); + } + + return getExistingRoots([path.dirname(packagePath)]); + } + + return null; +} + +function getRootsForFile(file, docsSource) { + const docsRoots = getRootsForDocsSource(docsSource, file); + if (docsRoots?.length) { + return docsRoots; + } + if (file?.path) { if (file.path.includes(path.join('pages', 'react-aria', 'internationalized'))) { - return `intl:${name}`; + return [INTL_SRC_ROOT, S2_SRC_ROOT, RAC_SRC_ROOT]; } else if (file.path.includes(path.join('pages', 'react-aria'))) { - return `rac:${name}`; - } else if (file.path.includes(path.join('pages', 's2'))) { - return `s2:${name}`; + return [RAC_SRC_ROOT, S2_SRC_ROOT, INTL_SRC_ROOT]; } } - return `default:${name}`; + return COMPONENT_SRC_ROOTS; } -function resolveComponentPath(componentName, file) { +function resolveComponentPath(componentName, file, docsSource) { // Check unified cache first - const cacheKey = getCacheKey(componentName, file); + const cacheKey = getCacheKey(`${docsSource || 'default'}:${componentName}:path`, file); if (interfacePathCache.has(cacheKey)) { return interfacePathCache.get(cacheKey); } - - let roots = getRootsForFile(file); + + let roots = getRootsForFile(file, docsSource); // Fast path: check direct file paths first for (let root of roots) { @@ -970,14 +1034,14 @@ function resolveComponentPath(componentName, file) { /** * Extract the leading JSDoc description comment placed immediately above the export for a component. */ -function getComponentDescription(componentName, file) { +function getComponentDescription(componentName, file, docsSource) { // Check cache first - const cacheKey = getCacheKey(componentName, file); + const cacheKey = getCacheKey(`${docsSource || 'default'}:${componentName}:description`, file); if (descriptionCache.has(cacheKey)) { return descriptionCache.get(cacheKey); } - const componentPath = resolveComponentPath(componentName, file); + const componentPath = resolveComponentPath(componentName, file, docsSource); if (!componentPath) { descriptionCache.set(cacheKey, null); return null; @@ -1123,13 +1187,13 @@ function parseFencedCodeBlock(example) { }; } -function getFunctionExamples(functionName, file) { - const cacheKey = getCacheKey(`${functionName}:examples`, file); +function getFunctionExamples(functionName, file, docsSource) { + const cacheKey = getCacheKey(`${docsSource || 'default'}:${functionName}:examples`, file); if (functionExamplesCache.has(cacheKey)) { return functionExamplesCache.get(cacheKey); } - const functionPath = resolveComponentPath(functionName, file); + const functionPath = resolveComponentPath(functionName, file, docsSource); if (!functionPath) { functionExamplesCache.set(cacheKey, []); return []; @@ -1438,11 +1502,38 @@ function generateInterfaceTable(interfaceName, file) { * Custom remark plugin that removes MDX import/export statements. */ function remarkRemoveImportsExports() { - return (tree) => { + return (tree, file) => { + let docsImports = {}; visit(tree, 'mdxjsEsm', (node, index, parent) => { + if (node.value) { + try { + const ast = babel.parse(node.value, { + sourceType: 'module', + plugins: ['jsx', 'typescript'] + }); + + for (const statement of ast.program.body) { + if (statement.type !== 'ImportDeclaration' || typeof statement.source.value !== 'string' || !statement.source.value.startsWith('docs:')) { + continue; + } + + const docsSource = statement.source.value.slice(5); + for (const specifier of statement.specifiers) { + if (specifier.local?.name) { + docsImports[specifier.local.name] = docsSource; + } + } + } + } catch { + // Ignore non-import ESM blocks. + } + } + parent.children.splice(index, 1); return index; }); + + file.data.docsImports = docsImports; }; } @@ -1655,10 +1746,17 @@ function remarkDocsComponentsToMarkdown() { if (name === 'FunctionJSDoc') { const functionAttr = node.attributes?.find((a) => a.name === 'function'); let functionName = null; + let docsSource = null; if (functionAttr && functionAttr.value?.type === 'mdxJsxAttributeValueExpression') { - const m = functionAttr.value.value.match(/\.exports\.([\w$]+)/); + const m = functionAttr.value.value.match(/^([\w$]+)\.exports\.([\w$]+)$/); if (m) { - functionName = m[1]; + docsSource = getDocsImportSource(m[1], file); + functionName = m[2]; + } else { + const fallback = functionAttr.value.value.match(/\.exports\.([\w$]+)/); + if (fallback) { + functionName = fallback[1]; + } } } @@ -1668,13 +1766,13 @@ function remarkDocsComponentsToMarkdown() { } const newNodes = []; - const description = getComponentDescription(functionName, file); + const description = getComponentDescription(functionName, file, docsSource); if (description) { const descTree = unified().use(remarkParse).parse(description); newNodes.push(...descTree.children); } - const examples = getFunctionExamples(functionName, file); + const examples = getFunctionExamples(functionName, file, docsSource); for (let [exampleIndex, example] of examples.entries()) { if (examples.length > 1) { newNodes.push({ From 302c704fd7602dd1c544b7568f72ae2744bd1136 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Mon, 9 Mar 2026 10:15:03 -0500 Subject: [PATCH 16/17] add linearGradient --- packages/@react-spectrum/s2/style/index.ts | 2 +- .../@react-spectrum/s2/style/spectrum-theme.ts | 17 +++++++++++++++++ packages/dev/s2-docs/pages/s2/style-macro.mdx | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/style/index.ts b/packages/@react-spectrum/s2/style/index.ts index 903adbd9a7b..719ab7b1bdc 100644 --- a/packages/@react-spectrum/s2/style/index.ts +++ b/packages/@react-spectrum/s2/style/index.ts @@ -15,7 +15,7 @@ import {Inset, fontRelative as internalFontRelative, space as internalSpace, Spa import type {MacroContext} from '@parcel/macros'; import {StyleString} from './types'; -export {baseColor, color, lightDark, colorMix, size, style} from './spectrum-theme'; +export {baseColor, color, lightDark, colorMix, linearGradient, size, style} from './spectrum-theme'; export {raw, keyframes} from './style-macro'; export type {StyleString} from './types'; diff --git a/packages/@react-spectrum/s2/style/spectrum-theme.ts b/packages/@react-spectrum/s2/style/spectrum-theme.ts index 0cf5302c724..441be4938d3 100644 --- a/packages/@react-spectrum/s2/style/spectrum-theme.ts +++ b/packages/@react-spectrum/s2/style/spectrum-theme.ts @@ -295,6 +295,23 @@ interface LinearGradient { stops: [SpectrumColor, number][] } +/** + * Creates a linear gradient value for use with the `backgroundImage` style property. + * Each color stop is registered as a CSS custom property so gradients can transition smoothly. + * + * @param angle - The CSS gradient direction or angle (e.g. `'to bottom right'`, `'45deg'`). + * @param tokens - Gradient color stops as `[color, percent]` tuples. + * @returns A gradient descriptor wrapped for use in the style macro. + * + * @example + * ```tsx + * import {linearGradient, style} from '@react-spectrum/s2/style' with {type: 'macro'}; + * + * const styles = style({ + * backgroundImage: linearGradient('to bottom right', ['fuchsia-900', 0], ['indigo-900', 66], ['blue-900', 100]) + * }); + * ``` + */ export function linearGradient(this: MacroContext | void, angle: string, ...tokens: [SpectrumColor, number][]): [LinearGradient] { // Generate @property rules for each gradient stop color. This allows the gradient to be animated. let propertyDefinitions: string[] = []; diff --git a/packages/dev/s2-docs/pages/s2/style-macro.mdx b/packages/dev/s2-docs/pages/s2/style-macro.mdx index 8e7b3bbd466..ccc94309167 100644 --- a/packages/dev/s2-docs/pages/s2/style-macro.mdx +++ b/packages/dev/s2-docs/pages/s2/style-macro.mdx @@ -79,6 +79,10 @@ The style macro system provides built-in utility functions for common patterns. +### linearGradient + + + ### size From 8cc7ebfe1c043fc496e7006f7929e00c39a859c6 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Tue, 10 Mar 2026 10:50:10 -0500 Subject: [PATCH 17/17] remove getAllowedOverrides from exports/docs (could have breaking changes) --- packages/@react-spectrum/s2/src/index.ts | 1 - .../@react-spectrum/s2/src/style-utils.ts | 25 ------------------- packages/dev/s2-docs/pages/s2/style-macro.mdx | 4 --- 3 files changed, 30 deletions(-) diff --git a/packages/@react-spectrum/s2/src/index.ts b/packages/@react-spectrum/s2/src/index.ts index e7205bf3467..4241bb8e699 100644 --- a/packages/@react-spectrum/s2/src/index.ts +++ b/packages/@react-spectrum/s2/src/index.ts @@ -93,7 +93,6 @@ export {TreeView, TreeViewItem, TreeViewItemContent, TreeViewLoadMoreItem} from export {pressScale} from './pressScale'; export { - getAllowedOverrides, centerPadding, setColorScheme } from './style-utils'; diff --git a/packages/@react-spectrum/s2/src/style-utils.ts b/packages/@react-spectrum/s2/src/style-utils.ts index 33af1c5da22..dbe5534e097 100644 --- a/packages/@react-spectrum/s2/src/style-utils.ts +++ b/packages/@react-spectrum/s2/src/style-utils.ts @@ -377,31 +377,6 @@ export interface StyleProps extends UnsafeStyles { styles?: StylesProp } -/** - * Returns the list of CSS property names that are allowed as style overrides via the `styles` prop. - * By default includes layout properties (margin, position, grid, etc.) and width properties. - * Optionally includes height properties. - * - * @param options - Configuration for which property groups to include. - * @param options.width - Whether to include width properties (`width`, `minWidth`, `maxWidth`). Defaults to `true`. - * @param options.height - Whether to include height properties (`height`, `minHeight`, `maxHeight`) and `size` (applies to both axes). Defaults to `false`. - * @returns An array of allowed CSS property names. - * - * @example - * ```tsx - * import {getAllowedOverrides} from '@react-spectrum/s2'; - * import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; - * - * const styles = style({ - * // ... component styles - * }, getAllowedOverrides()); - * - * // With height overrides enabled: - * const styles = style({ - * // ... component styles - * }, getAllowedOverrides({height: true})); - * ``` - */ export function getAllowedOverrides({width = true, height = false} = {}): string[] { return (allowedOverrides as unknown as string[]).concat(width ? widthProperties : []).concat(height ? heightProperties : []); } diff --git a/packages/dev/s2-docs/pages/s2/style-macro.mdx b/packages/dev/s2-docs/pages/s2/style-macro.mdx index ccc94309167..31e88deb1ea 100644 --- a/packages/dev/s2-docs/pages/s2/style-macro.mdx +++ b/packages/dev/s2-docs/pages/s2/style-macro.mdx @@ -116,10 +116,6 @@ See the [Icons](icons#api) page for more information. -### getAllowedOverrides - - - ### centerPadding