@@ -401,6 +401,159 @@ describe('ThemeProvider', () => {
401401 } ) ;
402402 } ) ;
403403
404+ describe ( 'light/dark mode brand color variants' , ( ) => {
405+ it ( 'should support object format with light/dark variants' , ( ) => {
406+ render (
407+ < ThemeProvider
408+ brandColors = { {
409+ primary : { light : '#6366F1' , dark : '#818CF8' } ,
410+ } }
411+ >
412+ < div > Test</ div >
413+ </ ThemeProvider >
414+ ) ;
415+
416+ const styleElement = document . querySelector ( '[data-ainativekit-brand-colors]' ) ;
417+ // Should contain both light and dark mode colors
418+ expect ( styleElement ?. textContent ) . toContain ( '#6366F1' ) ;
419+ expect ( styleElement ?. textContent ) . toContain ( '#818CF8' ) ;
420+ } ) ;
421+
422+ it ( 'should generate separate CSS blocks for light and dark themes' , ( ) => {
423+ render (
424+ < ThemeProvider
425+ brandColors = { {
426+ primary : { light : '#059669' , dark : '#34D399' } ,
427+ } }
428+ >
429+ < div > Test</ div >
430+ </ ThemeProvider >
431+ ) ;
432+
433+ const styleElement = document . querySelector ( '[data-ainativekit-brand-colors]' ) ;
434+ const css = styleElement ?. textContent || '' ;
435+
436+ // Should have light mode block
437+ expect ( css ) . toContain ( 'html[data-ainativekit-brand]' ) ;
438+ // Should have dark mode block
439+ expect ( css ) . toContain ( '[data-theme="dark"]' ) ;
440+ } ) ;
441+
442+ it ( 'should use same color for both modes when string value provided' , ( ) => {
443+ render (
444+ < ThemeProvider
445+ brandColors = { {
446+ primary : '#6366F1' , // Same for both modes
447+ } }
448+ >
449+ < div > Test</ div >
450+ </ ThemeProvider >
451+ ) ;
452+
453+ const styleElement = document . querySelector ( '[data-ainativekit-brand-colors]' ) ;
454+ const css = styleElement ?. textContent || '' ;
455+
456+ // Should appear in both light and dark blocks
457+ const matches = css . match ( / # 6 3 6 6 F 1 / g) ;
458+ expect ( matches ?. length ) . toBeGreaterThanOrEqual ( 2 ) ;
459+ } ) ;
460+
461+ it ( 'should not duplicate warnings for string brand colors' , ( ) => {
462+ consoleWarnSpy . mockClear ( ) ;
463+
464+ render (
465+ < ThemeProvider
466+ brandColors = { {
467+ primary : '#AAAAAA' , // Poor contrast - should warn only once
468+ } }
469+ >
470+ < div > Test</ div >
471+ </ ThemeProvider >
472+ ) ;
473+
474+ // Count warnings about contrast for primary color
475+ const contrastWarnings = consoleWarnSpy . mock . calls . filter (
476+ ( call ) =>
477+ call [ 0 ] . includes ( 'primary' ) && call [ 0 ] . includes ( 'WCAG AA contrast requirements' )
478+ ) ;
479+
480+ // Should only warn once, not twice (once for light, once for dark)
481+ expect ( contrastWarnings . length ) . toBe ( 1 ) ;
482+ } ) ;
483+
484+ it ( 'should warn separately for different light/dark colors in object format' , ( ) => {
485+ consoleWarnSpy . mockClear ( ) ;
486+
487+ render (
488+ < ThemeProvider
489+ brandColors = { {
490+ primary : {
491+ light : '#AAAAAA' , // Poor contrast in light mode
492+ dark : '#888888' , // Poor contrast in dark mode (different color)
493+ } ,
494+ } }
495+ >
496+ < div > Test</ div >
497+ </ ThemeProvider >
498+ ) ;
499+
500+ // Should warn twice - once for each different color
501+ const contrastWarnings = consoleWarnSpy . mock . calls . filter ( ( call ) =>
502+ call [ 0 ] . includes ( 'WCAG AA contrast requirements' )
503+ ) ;
504+
505+ expect ( contrastWarnings . length ) . toBe ( 2 ) ;
506+ } ) ;
507+
508+ it ( 'should support mixed string and object brand colors' , ( ) => {
509+ render (
510+ < ThemeProvider
511+ brandColors = { {
512+ primary : '#6366F1' , // String - same for both modes
513+ success : { light : '#059669' , dark : '#34D399' } , // Object - different per mode
514+ warning : { light : '#D97706' , dark : '#FBBF24' } ,
515+ error : '#DC2626' , // String - same for both modes
516+ } }
517+ >
518+ < div > Test</ div >
519+ </ ThemeProvider >
520+ ) ;
521+
522+ const styleElement = document . querySelector ( '[data-ainativekit-brand-colors]' ) ;
523+ const css = styleElement ?. textContent || '' ;
524+
525+ // Check string values appear (used in both modes)
526+ expect ( css ) . toContain ( '#6366F1' ) ;
527+ expect ( css ) . toContain ( '#DC2626' ) ;
528+
529+ // Check object values appear
530+ expect ( css ) . toContain ( '#059669' ) ; // success light
531+ expect ( css ) . toContain ( '#34D399' ) ; // success dark
532+ expect ( css ) . toContain ( '#D97706' ) ; // warning light
533+ expect ( css ) . toContain ( '#FBBF24' ) ; // warning dark
534+ } ) ;
535+
536+ it ( 'should apply correct hover/active mix colors per theme' , ( ) => {
537+ render (
538+ < ThemeProvider
539+ brandColors = { {
540+ primary : { light : '#6366F1' , dark : '#818CF8' } ,
541+ } }
542+ >
543+ < div > Test</ div >
544+ </ ThemeProvider >
545+ ) ;
546+
547+ const styleElement = document . querySelector ( '[data-ainativekit-brand-colors]' ) ;
548+ const css = styleElement ?. textContent || '' ;
549+
550+ // Light mode should mix with black
551+ expect ( css ) . toContain ( 'black' ) ;
552+ // Dark mode should mix with white
553+ expect ( css ) . toContain ( 'white' ) ;
554+ } ) ;
555+ } ) ;
556+
404557 describe ( 'hex color normalization (end-to-end)' , ( ) => {
405558 it ( 'should normalize colors without # prefix' , ( ) => {
406559 render (
0 commit comments