66 useSyncExternalStore
77} from 'react'
88import { compileCss } from '../compiler.ts'
9+ import { isCssColor } from '../colors.ts'
910import { evaluateCssxMediaQuery } from '../resolve.ts'
1011import type { CompiledCssSheet } from '../types.ts'
1112import {
@@ -15,7 +16,8 @@ import {
1516} from './config.ts'
1617import {
1718 coerceCssValue ,
18- resolveCssValue
19+ resolveCssValue ,
20+ type ResolveCssValueResult
1921} from '../values.ts'
2022import {
2123 createDependencySnapshot ,
@@ -36,6 +38,8 @@ const useCommitEffect = typeof window === 'undefined'
3638 ? useEffect
3739 : useLayoutEffect
3840const CSS_VARIABLE_NAME_RE = / ^ - - [ A - Z a - z 0 - 9 _ - ] + $ /
41+ const CSS_COLOR_FUNCTION_RE = / ^ (?: r g b a ? | h s l a ? | h w b | l a b | l c h | o k l a b | o k l c h | c o l o r | c o l o r - m i x ) \( / i
42+ const CSS_COLOR_TOKEN_RE = / ^ [ A - Z a - z ] [ A - Z a - z 0 - 9 _ - ] * $ /
3943const DEFAULT_CUSTOM_MEDIA : Record < string , string > = {
4044 '--breakpoint-mobile' : '(width < 48rem)' ,
4145 '--breakpoint-tablet' : '(width >= 48rem)' ,
@@ -87,6 +91,14 @@ export type CssxLayerHookOutput =
8791 | undefined
8892 | false
8993
94+ export type CssColorMixInput =
95+ | number
96+ | string
97+ | {
98+ mix ?: number | string
99+ with ?: string
100+ }
101+
90102export function useCssxSheet (
91103 sheet : CompiledCssSheet ,
92104 options : CssxReactConfig = { }
@@ -192,7 +204,7 @@ export function useCssVariableRaw (
192204 const context = useCssxRuntimeContext ( )
193205 const committedDependenciesRef = useRef < CssxDependencySnapshot > ( createDependencySnapshot ( ) )
194206 const result = resolveCssVariableRaw ( name , fallback , context . scopedVariables )
195- const renderDependencies = createVariableDependencySnapshot ( result )
207+ const renderDependencies = createCssValueDependencySnapshot ( result )
196208
197209 useSyncExternalStore (
198210 listener => subscribeRuntimeStore ( listener , ( ) => committedDependenciesRef . current ) ,
@@ -215,6 +227,28 @@ export function useCssVariable (
215227 return value == null ? value : coerceCssValue ( value )
216228}
217229
230+ export function useCssColor (
231+ color : string ,
232+ mix ?: CssColorMixInput
233+ ) : string | undefined {
234+ const context = useCssxRuntimeContext ( )
235+ const committedDependenciesRef = useRef < CssxDependencySnapshot > ( createDependencySnapshot ( ) )
236+ const result = resolveCssColor ( color , mix , context . scopedVariables )
237+ const renderDependencies = createCssValueDependencySnapshot ( result )
238+
239+ useSyncExternalStore (
240+ listener => subscribeRuntimeStore ( listener , ( ) => committedDependenciesRef . current ) ,
241+ getRuntimeVersion ,
242+ getRuntimeVersion
243+ )
244+
245+ useCommitEffect ( ( ) => {
246+ committedDependenciesRef . current = renderDependencies
247+ } )
248+
249+ return result . value
250+ }
251+
218252export function getCssVariableRaw (
219253 name : string ,
220254 fallback ?: unknown
@@ -231,6 +265,13 @@ export function getCssVariable (
231265 return value == null ? value : coerceCssValue ( value )
232266}
233267
268+ export function getCssColor (
269+ color : string ,
270+ mix ?: CssColorMixInput
271+ ) : string | undefined {
272+ return resolveCssColor ( color , mix ) . value
273+ }
274+
234275export function useMedia ( ) : Record < string , boolean > {
235276 const context = useCssxRuntimeContext ( )
236277 const committedDependenciesRef = useRef < CssxDependencySnapshot > ( createDependencySnapshot ( ) )
@@ -367,8 +408,21 @@ function resolveCssVariableRaw (
367408 } )
368409}
369410
370- function createVariableDependencySnapshot (
371- result : ReturnType < typeof resolveCssVariableRaw >
411+ function resolveCssColor (
412+ color : string ,
413+ mix ?: CssColorMixInput ,
414+ scopedVariables ?: readonly Record < string , unknown > [ ]
415+ ) : ResolveCssValueResult {
416+ return resolveCssValue ( createCssColorExpression ( color , mix ) , {
417+ variables : getVariableValues ( ) ,
418+ scopedVariables,
419+ defaultVariables : getDefaultVariableValues ( ) ,
420+ dimensions : getDimensions ( )
421+ } )
422+ }
423+
424+ function createCssValueDependencySnapshot (
425+ result : ResolveCssValueResult
372426) : CssxDependencySnapshot {
373427 const dependencies = createDependencySnapshot ( )
374428 for ( const name of result . dependencies . vars ) {
@@ -380,6 +434,67 @@ function createVariableDependencySnapshot (
380434 return dependencies
381435}
382436
437+ function createCssColorExpression (
438+ color : string ,
439+ mix ?: CssColorMixInput
440+ ) : string {
441+ const base = normalizeCssColorExpression ( color )
442+ const mixOptions = normalizeColorMix ( mix )
443+ if ( mixOptions == null ) return base
444+
445+ return `color-mix(in srgb, ${ base } ${ mixOptions . weight } , ${ normalizeCssColorExpression ( mixOptions . with ) } )`
446+ }
447+
448+ function normalizeCssColorExpression ( input : string ) : string {
449+ const value = input . trim ( )
450+ if ( value === '' ) return value
451+ if ( / ^ v a r \( / i. test ( value ) ) return value
452+ if ( value . startsWith ( '--' ) ) {
453+ throw new TypeError ( `Ambiguous CSS color token "${ input } ". Use "var(${ value } )" or a semantic token such as "primary".` )
454+ }
455+ if (
456+ CSS_COLOR_FUNCTION_RE . test ( value ) ||
457+ isCssColor ( value ) ||
458+ ! CSS_COLOR_TOKEN_RE . test ( value )
459+ ) {
460+ return value
461+ }
462+
463+ return `var(--color-${ value } )`
464+ }
465+
466+ function normalizeColorMix (
467+ input : CssColorMixInput | undefined
468+ ) : { weight : string , with : string } | null {
469+ if ( input == null ) return null
470+ if ( typeof input === 'number' || typeof input === 'string' ) {
471+ return {
472+ weight : normalizeMixWeight ( input ) ,
473+ with : 'transparent'
474+ }
475+ }
476+
477+ if ( input . mix == null ) return null
478+ return {
479+ weight : normalizeMixWeight ( input . mix ) ,
480+ with : input . with ?? 'transparent'
481+ }
482+ }
483+
484+ function normalizeMixWeight ( input : number | string ) : string {
485+ if ( typeof input === 'string' ) {
486+ const value = input . trim ( )
487+ if ( / ^ (?: \d + | \d * \. \d + ) % $ / . test ( value ) ) return value
488+ throw new TypeError ( `Invalid CSS color mix weight "${ input } ". Expected a percentage string such as "15%".` )
489+ }
490+
491+ if ( ! Number . isFinite ( input ) || input < 0 || input > 1 ) {
492+ throw new TypeError ( `Invalid CSS color mix weight "${ input } ". Expected a number from 0 to 1.` )
493+ }
494+
495+ return `${ input * 100 } %`
496+ }
497+
383498function resolveMedia (
384499 media : Record < string , string > ,
385500 context : ReturnType < typeof useCssxRuntimeContext >
0 commit comments