diff --git a/packages/mix/CHANGELOG.md b/packages/mix/CHANGELOG.md index 64a33bdb5..99d3ff12b 100644 --- a/packages/mix/CHANGELOG.md +++ b/packages/mix/CHANGELOG.md @@ -21,6 +21,14 @@ and tightens public API around the internal token registry. in order. - **`Prop.value` null safety:** the token-ref detection branch no longer crashes when `V` is nullable and the supplied value is `null`. +- **Shadow tokens through styler methods:** `boxShadowToken.mix()` / + `shadowToken.mix()` can now be passed directly to `BoxStyler.boxShadows`, + `BoxStyler.shadows`, `FlexBoxStyler.shadows`, `StackBoxStyler.shadows`, and + `TextStyler.shadows` (#925). The token refs now also implement the raw + `List` / `List` parameter types — the same + sentinel-style shim used for `DoubleRef` — so the styler signatures are + unchanged and existing literal-list call sites keep working. Token refs are + detected and routed as a `Mix` so their token source survives resolution. ### API changes diff --git a/packages/mix/lib/src/properties/painting/decoration_mix.dart b/packages/mix/lib/src/properties/painting/decoration_mix.dart index b3ca09d6f..04176f12f 100644 --- a/packages/mix/lib/src/properties/painting/decoration_mix.dart +++ b/packages/mix/lib/src/properties/painting/decoration_mix.dart @@ -141,8 +141,15 @@ final class BoxDecorationMix extends DecorationMix color: Prop.maybe(color), image: Prop.maybeMix(image), gradient: Prop.maybeMix(gradient), + // A `boxShadowToken.mix()` ref already is a [BoxShadowListMix] (and a + // token-carrying [Prop]); pass it straight to [Prop.mix] so its token + // source is preserved instead of being wrapped into a fresh list. boxShadow: boxShadow != null - ? Prop.mix(BoxShadowListMix(boxShadow)) + ? Prop.mix( + boxShadow is BoxShadowListMix + ? boxShadow as BoxShadowListMix + : BoxShadowListMix(boxShadow), + ) : null, ); diff --git a/packages/mix/lib/src/properties/typography/text_style_mix.dart b/packages/mix/lib/src/properties/typography/text_style_mix.dart index 33de0e367..e8d28503e 100644 --- a/packages/mix/lib/src/properties/typography/text_style_mix.dart +++ b/packages/mix/lib/src/properties/typography/text_style_mix.dart @@ -212,7 +212,16 @@ class TextStyleMix extends Mix debugLabel: Prop.maybe(debugLabel), wordSpacing: Prop.maybe(wordSpacing), textBaseline: Prop.maybe(textBaseline), - shadows: shadows != null ? Prop.mix(ShadowListMix(shadows)) : null, + // A `shadowToken.mix()` ref already is a [ShadowListMix] (and a + // token-carrying [Prop]); pass it straight to [Prop.mix] so its token + // source is preserved instead of being wrapped into a fresh list. + shadows: shadows != null + ? Prop.mix( + shadows is ShadowListMix + ? shadows as ShadowListMix + : ShadowListMix(shadows), + ) + : null, fontFeatures: Prop.maybe(fontFeatures), decoration: Prop.maybe(decoration), decorationColor: Prop.maybe(decorationColor), diff --git a/packages/mix/lib/src/theme/tokens/token_refs.dart b/packages/mix/lib/src/theme/tokens/token_refs.dart index ca9eaae9e..0b10dec4e 100644 --- a/packages/mix/lib/src/theme/tokens/token_refs.dart +++ b/packages/mix/lib/src/theme/tokens/token_refs.dart @@ -165,10 +165,15 @@ final class BoxShadowMixRef extends Prop } } -/// Token reference for [ShadowListMix] that implements Mix interface instead of Flutter interface +/// Token reference for [ShadowListMix] that implements Mix interface instead of Flutter interface. +/// +/// Also implements `List` so that `shadowToken.mix()` can be passed +/// directly to styler methods that accept a raw `List` (e.g. +/// `TextStyler.shadows`) without changing their signatures. The list members +/// are never invoked — the ref is detected as a token and routed as a Mix. final class ShadowListMixRef extends Prop> with ValueRef> - implements ShadowListMix { + implements ShadowListMix, List { ShadowListMixRef(super.prop) : super.fromProp(); @override @@ -177,10 +182,16 @@ final class ShadowListMixRef extends Prop> } } -/// Token reference for [BoxShadowListMix] that implements Mix interface instead of Flutter interface +/// Token reference for [BoxShadowListMix] that implements Mix interface instead of Flutter interface. +/// +/// Also implements `List` so that `boxShadowToken.mix()` can be +/// passed directly to styler methods that accept a raw `List` +/// (e.g. `BoxStyler.boxShadows`, `BoxStyler.shadows`) without changing their +/// signatures. The list members are never invoked — the ref is detected as a +/// token and routed as a Mix. final class BoxShadowListMixRef extends Prop> with ValueRef> - implements BoxShadowListMix { + implements BoxShadowListMix, List { BoxShadowListMixRef(super.prop) : super.fromProp(); @override diff --git a/packages/mix/test/src/theme/tokens/shadow_list_token_integration_test.dart b/packages/mix/test/src/theme/tokens/shadow_list_token_integration_test.dart index 7d4283c46..b7e38a42e 100644 --- a/packages/mix/test/src/theme/tokens/shadow_list_token_integration_test.dart +++ b/packages/mix/test/src/theme/tokens/shadow_list_token_integration_test.dart @@ -128,4 +128,132 @@ void main() { expect(boxShadowRef.runtimeType, equals(BoxShadowListRef)); }); }); + + group('Shadow token .mix() through styler methods (non-breaking)', () { + test('BoxShadowToken.mix() also implements List', () { + const boxShadowToken = BoxShadowToken('test.box.shadows.mix'); + final mixRef = boxShadowToken.mix(); + + // The ref satisfies the raw list parameter type AND is a Mix. + expect(mixRef, isA>()); + expect(mixRef, isA()); + expect(isAnyTokenRef(mixRef), isTrue); + }); + + test('ShadowToken.mix() also implements List', () { + const shadowToken = ShadowToken('test.shadows.mix'); + final mixRef = shadowToken.mix(); + + expect(mixRef, isA>()); + expect(mixRef, isA()); + expect(isAnyTokenRef(mixRef), isTrue); + }); + + test('BoxStyler.boxShadows accepts BoxShadowToken.mix()', () { + const boxShadowToken = BoxShadowToken('test.box.shadows.boxShadows'); + + // Compiles against the unchanged List signature. + final styler = BoxStyler().boxShadows(boxShadowToken.mix()); + + expect(styler.$decoration, isNotNull); + }); + + test('BoxStyler.shadows accepts BoxShadowToken.mix()', () { + const boxShadowToken = BoxShadowToken('test.box.shadows.shadows'); + + final styler = BoxStyler().shadows(boxShadowToken.mix()); + + expect(styler.$decoration, isNotNull); + }); + + test( + 'BoxStyler.boxShadows still accepts a literal list (non-breaking)', + () { + final styler = BoxStyler().boxShadows([ + BoxShadowMix(color: Colors.black, blurRadius: 5), + BoxShadowMix(color: Colors.grey, blurRadius: 10), + ]); + + expect(styler.$decoration, isNotNull); + }, + ); + + testWidgets( + 'BoxStyler.boxShadows resolves BoxShadowToken.mix() through MixScope', + (tester) async { + const boxShadowToken = BoxShadowToken('box.shadows.token-mix.resolved'); + final testBoxShadows = [ + const BoxShadow(color: Colors.black, blurRadius: 4), + const BoxShadow(color: Colors.grey, blurRadius: 2), + ]; + + await tester.pumpWidget( + MixScope( + tokens: {boxShadowToken: testBoxShadows}, + child: Builder( + builder: (context) { + final styler = BoxStyler().boxShadows(boxShadowToken.mix()); + final styleSpec = styler.resolve(context); + final decoration = styleSpec.spec.decoration; + + expect(decoration, isA()); + expect( + (decoration as BoxDecoration).boxShadow, + equals(testBoxShadows), + ); + + return const SizedBox.shrink(); + }, + ), + ), + ); + }, + ); + + test('TextStyler.shadows accepts ShadowToken.mix()', () { + const shadowToken = ShadowToken('text.shadows.mix'); + + final styler = TextStyler().shadows(shadowToken.mix()); + + expect(styler.$style, isNotNull); + }); + + test('TextStyler.shadows still accepts a literal list (non-breaking)', () { + final styler = TextStyler().shadows([ + ShadowMix(color: Colors.black, offset: const Offset(1, 1)), + ]); + + expect(styler.$style, isNotNull); + }); + + testWidgets( + 'TextStyler.shadows resolves ShadowToken.mix() through MixScope', + (tester) async { + const shadowToken = ShadowToken('text.shadows.token-mix.resolved'); + final testShadows = [ + const Shadow( + color: Colors.black, + offset: Offset(2, 2), + blurRadius: 4, + ), + ]; + + await tester.pumpWidget( + MixScope( + tokens: {shadowToken: testShadows}, + child: Builder( + builder: (context) { + final styler = TextStyler().shadows(shadowToken.mix()); + final styleSpec = styler.resolve(context); + + expect(styleSpec.spec.style?.shadows, equals(testShadows)); + + return const SizedBox.shrink(); + }, + ), + ), + ); + }, + ); + }); }