Skip to content
8 changes: 8 additions & 0 deletions packages/mix/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<BoxShadowMix>` / `List<ShadowMix>` 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

Expand Down
9 changes: 8 additions & 1 deletion packages/mix/lib/src/properties/painting/decoration_mix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,15 @@ final class BoxDecorationMix extends DecorationMix<BoxDecoration>
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch currently blocks CI: with dcm analyze --fatal-warnings, boxShadow is statically List<BoxShadowMix>?, so DCM reports boxShadow is BoxShadowListMix and the following cast as unrelated. The same pattern in TextStyleMix fails too. Please route this through a DCM-safe helper or another preservation path so the token case survives without tripping analysis.

? boxShadow as BoxShadowListMix
: BoxShadowListMix(boxShadow),
)
: null,
);

Expand Down
11 changes: 10 additions & 1 deletion packages/mix/lib/src/properties/typography/text_style_mix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,16 @@ class TextStyleMix extends Mix<TextStyle>
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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This preserves pure token .mix() values, but merge resolution still loses token fields when a token list is chained with a literal list. In Prop.resolveProp, once sources include both TokenSource and MixSource, the resolved List<Shadow> must be converted back to Mix<List<Shadow>>; only individual Shadow/BoxShadow converters are registered today, so the token list is skipped. Please add list converters and tests for literal-to-token and token-to-literal merge order.

shadows is ShadowListMix
? shadows as ShadowListMix
: ShadowListMix(shadows),
)
: null,
fontFeatures: Prop.maybe(fontFeatures),
decoration: Prop.maybe(decoration),
decorationColor: Prop.maybe(decorationColor),
Expand Down
19 changes: 15 additions & 4 deletions packages/mix/lib/src/theme/tokens/token_refs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,15 @@ final class BoxShadowMixRef extends Prop<BoxShadow>
}
}

/// 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<ShadowMix>` so that `shadowToken.mix()` can be passed
/// directly to styler methods that accept a raw `List<ShadowMix>` (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<List<Shadow>>
with ValueRef<List<Shadow>>
implements ShadowListMix {
implements ShadowListMix, List<ShadowMix> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this ref now implements the raw List<ShadowMix> type, it can be passed to every existing List<ShadowMix> constructor, not just TextStyler.shadows. Generated IconStyler(shadows:) and IconThemeModifierMix(shadows:) still do Prop.mix(ShadowListMix(shadows)), which wraps this token ref as a list item and then tries to iterate it during resolve. Please apply the same token-preserving path to those raw-list shadow constructors, including the generated source for IconStyler.

ShadowListMixRef(super.prop) : super.fromProp();

@override
Expand All @@ -177,10 +182,16 @@ final class ShadowListMixRef extends Prop<List<Shadow>>
}
}

/// 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<BoxShadowMix>` so that `boxShadowToken.mix()` can be
/// passed directly to styler methods that accept a raw `List<BoxShadowMix>`
/// (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<List<BoxShadow>>
with ValueRef<List<BoxShadow>>
implements BoxShadowListMix {
implements BoxShadowListMix, List<BoxShadowMix> {
BoxShadowListMixRef(super.prop) : super.fromProp();

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BoxShadowMix>', () {
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<List<BoxShadowMix>>());
expect(mixRef, isA<BoxShadowListMix>());
expect(isAnyTokenRef(mixRef), isTrue);
});

test('ShadowToken.mix() also implements List<ShadowMix>', () {
const shadowToken = ShadowToken('test.shadows.mix');
final mixRef = shadowToken.mix();

expect(mixRef, isA<List<ShadowMix>>());
expect(mixRef, isA<ShadowListMix>());
expect(isAnyTokenRef(mixRef), isTrue);
});

test('BoxStyler.boxShadows accepts BoxShadowToken.mix()', () {
const boxShadowToken = BoxShadowToken('test.box.shadows.boxShadows');

// Compiles against the unchanged List<BoxShadowMix> 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<BoxDecoration>());
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();
},
),
),
);
},
);
});
}
Loading