From 0b966248a3f9a9c255c2f95c7e8a703d03c7187d Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 22 Jun 2026 11:00:13 -0300 Subject: [PATCH 1/8] fix(mix): accept shadow token .mix() on raw-list styler methods via sentinel-style refs Make BoxShadowListMixRef / ShadowListMixRef also implement List / List so boxShadowToken.mix() and shadowToken.mix() can be passed directly to BoxStyler.boxShadows, BoxStyler.shadows, FlexBoxStyler.shadows, StackBoxStyler.shadows, and TextStyler.shadows. This mirrors the DoubleRef sentinel shim: the public signatures stay as raw lists, the token ref flows through as that type, and the styler detects it and routes it as a Mix so the TokenSource survives resolution. Unlike the *ListMix-wrapper approach, this keeps the styler signatures unchanged, so existing literal-list call sites, generated factories, and mix_tailwinds need no changes. Fixes #925. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/mix/CHANGELOG.md | 8 ++ .../style/mixins/decoration_style_mixin.dart | 12 +- .../src/style/mixins/shadow_style_mixin.dart | 12 +- .../src/style/mixins/text_style_mixin.dart | 12 +- .../mix/lib/src/theme/tokens/token_refs.dart | 19 ++- .../shadow_list_token_integration_test.dart | 120 ++++++++++++++++++ 6 files changed, 176 insertions(+), 7 deletions(-) diff --git a/packages/mix/CHANGELOG.md b/packages/mix/CHANGELOG.md index 64a33bdb59..99d3ff12ba 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/style/mixins/decoration_style_mixin.dart b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart index b362f7d5b5..f56da1a2a8 100644 --- a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; +import '../../core/prop.dart'; import '../../properties/painting/border_mix.dart'; import '../../properties/painting/border_radius_mix.dart'; import '../../properties/painting/decoration_image_mix.dart'; @@ -39,8 +40,17 @@ mixin DecorationStyleMixin> { return decoration(BoxDecorationMix.boxShadow([value])); } - /// Sets multiple shadows + /// Sets multiple shadows. + /// + /// A design-token reference (`boxShadowToken.mix()`) arrives here as a + /// [BoxShadowListMix] (it implements both `List` and + /// [BoxShadowListMix]); it is routed as a Mix so its token source is + /// preserved. A literal list is wrapped as usual. T shadows(List value) { + if (value case final BoxShadowListMix ref) { + return decoration(BoxDecorationMix.create(boxShadow: Prop.mix(ref))); + } + return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart index 8d23f136ed..f92329f166 100644 --- a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/mix_element.dart'; +import '../../core/prop.dart'; import '../../properties/painting/decoration_mix.dart'; import '../../properties/painting/shadow_mix.dart'; @@ -26,8 +27,17 @@ mixin ShadowStyleMixin> { return decoration(BoxDecorationMix.boxShadow([shadow])); } - /// Creates multiple box shadows from a list of BoxShadowMix + /// Creates multiple box shadows from a list of BoxShadowMix. + /// + /// A design-token reference (`boxShadowToken.mix()`) arrives here as a + /// [BoxShadowListMix] (it implements both `List` and + /// [BoxShadowListMix]); it is routed as a Mix so its token source is + /// preserved. A literal list is wrapped as usual. T boxShadows(List value) { + if (value case final BoxShadowListMix ref) { + return decoration(BoxDecorationMix.create(boxShadow: Prop.mix(ref))); + } + return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/text_style_mixin.dart b/packages/mix/lib/src/style/mixins/text_style_mixin.dart index f10111dedd..12747c31b0 100644 --- a/packages/mix/lib/src/style/mixins/text_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/text_style_mixin.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; +import '../../core/prop.dart'; import '../../properties/painting/shadow_mix.dart'; import '../../properties/typography/text_style_mix.dart'; @@ -84,8 +85,17 @@ mixin TextStyleMixin> { return style(TextStyleMix.fontFamilyFallback(value)); } - /// Sets text shadows + /// Sets text shadows. + /// + /// A design-token reference (`shadowToken.mix()`) arrives here as a + /// [ShadowListMix] (it implements both `List` and [ShadowListMix]); + /// it is routed as a Mix so its token source is preserved. A literal list is + /// wrapped as usual. T shadows(List value) { + if (value case final ShadowListMix ref) { + return style(TextStyleMix.create(shadows: Prop.mix(ref))); + } + return style(TextStyleMix.shadows(value)); } diff --git a/packages/mix/lib/src/theme/tokens/token_refs.dart b/packages/mix/lib/src/theme/tokens/token_refs.dart index ca9eaae9ec..0b10dec4e9 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 7d4283c46f..a093720e0c 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,124 @@ 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(); + }, + ), + ), + ); + }); + }); } From 6f458a40ab1abebc0568254ffe4bcc6df490df71 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 22 Jun 2026 11:23:41 -0300 Subject: [PATCH 2/8] Update shadow_list_token_integration_test.dart --- .../shadow_list_token_integration_test.dart | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) 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 a093720e0c..b7e38a42e2 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 @@ -166,14 +166,17 @@ void main() { 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); - }); + 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', @@ -223,29 +226,34 @@ void main() { 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), - ]; + 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); + 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)); + expect(styleSpec.spec.style?.shadows, equals(testShadows)); - return const SizedBox.shrink(); - }, + return const SizedBox.shrink(); + }, + ), ), - ), - ); - }); + ); + }, + ); }); } From bcd6f01241fd35d3e77a14b0af836d434d33d02f Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 22 Jun 2026 11:39:31 -0300 Subject: [PATCH 3/8] docs(mix): add shadow styling example app demonstrating ShadowListMix and token .mix() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example app under packages/mix/example shows applying box and text shadow lists two ways: as literal List/List, and via BoxShadowToken/ShadowToken `.mix()` references (BoxShadowListMix / ShadowListMix) resolved through MixScope — the flow enabled by this branch. Wires the in-repo mix / mix_annotations packages via dependency_overrides so the example resolves standalone, without requiring melos bootstrap. Includes a widget smoke test. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/mix/example/README.md | 23 ++ packages/mix/example/lib/main.dart | 237 +++++++++++++++++++++ packages/mix/example/pubspec.yaml | 31 +++ packages/mix/example/test/widget_test.dart | 29 +++ 4 files changed, 320 insertions(+) create mode 100644 packages/mix/example/README.md create mode 100644 packages/mix/example/lib/main.dart create mode 100644 packages/mix/example/pubspec.yaml create mode 100644 packages/mix/example/test/widget_test.dart diff --git a/packages/mix/example/README.md b/packages/mix/example/README.md new file mode 100644 index 0000000000..50a0074df5 --- /dev/null +++ b/packages/mix/example/README.md @@ -0,0 +1,23 @@ +# mix_example + +A small demo app for Mix shadow styling. + +It shows the two ways to apply shadow lists with the styler shadow methods +(`BoxStyler.boxShadows` / `BoxStyler.shadows` / `TextStyler.shadows`): + +1. **Literal lists** — pass a `List` / `List` directly. +2. **Design tokens** — define a `BoxShadowToken` / `ShadowToken`, bind its value + once in a `MixScope`, and pass `token.mix()` (a `BoxShadowListMix` / + `ShadowListMix` reference) straight into the same method. + +See [`lib/main.dart`](lib/main.dart). + +## Run + +```bash +flutter run # from packages/mix/example +flutter test # widget smoke test +``` + +The in-repo `mix` / `mix_annotations` packages are wired via `dependency_overrides` +in `pubspec.yaml`, so the example builds without `melos bootstrap`. diff --git a/packages/mix/example/lib/main.dart b/packages/mix/example/lib/main.dart new file mode 100644 index 0000000000..6143174297 --- /dev/null +++ b/packages/mix/example/lib/main.dart @@ -0,0 +1,237 @@ +// ABOUTME: Demo app for Mix shadow styling. +// ABOUTME: Shows BoxShadowListMix / ShadowListMix via literal lists and via +// design tokens resolved with `token.mix()`. +import 'package:flutter/material.dart'; +import 'package:mix/mix.dart'; + +void main() => runApp(const ShadowExampleApp()); + +// ----------------------------------------------------------------------------- +// Design tokens. +// +// A `BoxShadowToken` / `ShadowToken` is a named handle for a list of shadows. +// `token.mix()` returns a Mix-compatible reference (a `BoxShadowListMix` / +// `ShadowListMix`) that can be passed straight into the styler shadow methods. +// The actual values are supplied once, at the top of the tree, via `MixScope`. +// ----------------------------------------------------------------------------- +const cardShadow = BoxShadowToken('shadow.card'); +const floatingShadow = BoxShadowToken('shadow.floating'); +const headlineGlow = ShadowToken('shadow.headline-glow'); + +class ShadowExampleApp extends StatelessWidget { + const ShadowExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Mix Shadow Example', + debugShowCheckedModeBanner: false, + home: MixScope( + // Bind each token to its resolved value. Every `token.mix()` used below + // resolves through this scope. + boxShadows: { + cardShadow: const [ + BoxShadow( + color: Color(0x1F000000), + offset: Offset(0, 2), + blurRadius: 8, + ), + BoxShadow( + color: Color(0x14000000), + offset: Offset(0, 1), + blurRadius: 3, + ), + ], + floatingShadow: const [ + BoxShadow( + color: Color(0x33000000), + offset: Offset(0, 12), + blurRadius: 28, + spreadRadius: -6, + ), + ], + }, + shadows: { + headlineGlow: const [ + Shadow(color: Color(0xAA4527A0), offset: Offset(0, 0), blurRadius: 18), + ], + }, + child: const ShadowGallery(), + ), + ); + } +} + +class ShadowGallery extends StatelessWidget { + const ShadowGallery({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF3F4F6), + appBar: AppBar(title: const Text('Mix · Shadow styling')), + body: ListView( + padding: const EdgeInsets.all(24), + children: const [ + _SectionLabel('1 · Literal box shadows (List)'), + _LiteralBoxShadowCard(), + SizedBox(height: 32), + + _SectionLabel('2 · Box shadow token via cardShadow.mix()'), + _TokenBoxShadowCard(), + SizedBox(height: 32), + + _SectionLabel('3 · Elevated token via floatingShadow.mix()'), + _FloatingTokenCard(), + SizedBox(height: 32), + + _SectionLabel('4 · Text shadows (literal + headlineGlow.mix())'), + _TextShadowSample(), + ], + ), + ); + } +} + +/// A box styled with a literal list of [BoxShadowMix] passed to `boxShadows`. +/// +/// Internally Mix wraps this list in a [BoxShadowListMix]; you can also pass an +/// explicit one (e.g. from a token) — see [_TokenBoxShadowCard]. +class _LiteralBoxShadowCard extends StatelessWidget { + const _LiteralBoxShadowCard(); + + @override + Widget build(BuildContext context) { + final style = BoxStyler() + .color(Colors.white) + .height(96) + .borderRounded(16) + .boxShadows([ + BoxShadowMix( + color: Colors.black.withValues(alpha: 0.18), + offset: const Offset(0, 6), + blurRadius: 16, + ), + BoxShadowMix( + color: Colors.indigo.withValues(alpha: 0.20), + offset: const Offset(0, 2), + blurRadius: 4, + ), + ]); + + return Box(style: style, child: const _CardLabel('boxShadows([...])')); + } +} + +/// A box whose shadow comes from the [cardShadow] design token. +/// +/// `cardShadow.mix()` returns a `BoxShadowListMix` reference; the styler keeps +/// its plain `List` signature, so the token flows straight in and +/// resolves against the value registered in [MixScope]. +class _TokenBoxShadowCard extends StatelessWidget { + const _TokenBoxShadowCard(); + + @override + Widget build(BuildContext context) { + final style = BoxStyler() + .color(Colors.white) + .height(96) + .borderRounded(16) + .boxShadows(cardShadow.mix()); + + return Box(style: style, child: const _CardLabel('boxShadows(cardShadow.mix())')); + } +} + +/// Same token mechanism, with a more pronounced "floating" elevation token. +class _FloatingTokenCard extends StatelessWidget { + const _FloatingTokenCard(); + + @override + Widget build(BuildContext context) { + final style = BoxStyler() + .color(Colors.white) + .height(96) + .borderRounded(20) + // `shadows` is the DecorationStyleMixin alias for `boxShadows`. + .shadows(floatingShadow.mix()); + + return Box(style: style, child: const _CardLabel('shadows(floatingShadow.mix())')); + } +} + +/// Text using a literal [ShadowMix] list and a token-backed glow. +class _TextShadowSample extends StatelessWidget { + const _TextShadowSample(); + + @override + Widget build(BuildContext context) { + final literal = TextStyler() + .fontSize(28) + .fontWeight(FontWeight.w800) + .color(Colors.black87) + .shadows([ + ShadowMix( + color: Colors.black.withValues(alpha: 0.35), + offset: const Offset(2, 2), + blurRadius: 4, + ), + ]); + + final glow = TextStyler() + .fontSize(28) + .fontWeight(FontWeight.w800) + .color(Colors.deepPurple) + .shadows(headlineGlow.mix()); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StyledText('Literal shadow', style: literal), + const SizedBox(height: 16), + StyledText('Token glow', style: glow), + ], + ); + } +} + +class _SectionLabel extends StatelessWidget { + const _SectionLabel(this.text); + + final String text; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text( + text, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFF6B7280), + ), + ), + ); + } +} + +class _CardLabel extends StatelessWidget { + const _CardLabel(this.code); + + final String code; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + code, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 13, + color: Color(0xFF374151), + ), + ), + ); + } +} diff --git a/packages/mix/example/pubspec.yaml b/packages/mix/example/pubspec.yaml new file mode 100644 index 0000000000..69411972e8 --- /dev/null +++ b/packages/mix/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: mix_example +description: "Demo app showcasing Mix shadow styling: BoxShadowListMix, ShadowListMix, and shadow tokens through .mix()." +publish_to: "none" +version: 0.1.0+1 + +environment: + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" + +dependencies: + flutter: + sdk: flutter + mix: + path: .. + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +# Resolve the in-repo packages by path so the example builds standalone, +# without requiring `melos bootstrap`. Melos also manages these via the +# (git-ignored) pubspec_overrides.yaml, which takes precedence when present. +dependency_overrides: + mix: + path: .. + mix_annotations: + path: ../../mix_annotations + +flutter: + uses-material-design: true diff --git a/packages/mix/example/test/widget_test.dart b/packages/mix/example/test/widget_test.dart new file mode 100644 index 0000000000..466448aefd --- /dev/null +++ b/packages/mix/example/test/widget_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix/mix.dart'; +import 'package:mix_example/main.dart'; + +void main() { + testWidgets('shadow gallery renders and resolves token shadows', ( + tester, + ) async { + await tester.pumpWidget(const ShadowExampleApp()); + await tester.pumpAndSettle(); + + // Section labels are present. + expect(find.textContaining('Literal box shadows'), findsOneWidget); + expect(find.textContaining('cardShadow.mix()'), findsWidgets); + + // The token-backed box resolves to a real BoxDecoration with the shadows + // registered in MixScope (2 box shadows for cardShadow). + final decorated = tester.widget( + find.descendant( + of: find.byType(Box), + matching: find.byType(Container), + ).first, + ); + final decoration = decorated.decoration as BoxDecoration; + expect(decoration.boxShadow, isNotNull); + expect(decoration.boxShadow!.isNotEmpty, isTrue); + }); +} From ace4b28760b1b012c387ec5b51caa28f7e4b37f2 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 22 Jun 2026 11:42:20 -0300 Subject: [PATCH 4/8] Revert "docs(mix): add shadow styling example app demonstrating ShadowListMix and token .mix()" This reverts commit bcd6f01241fd35d3e77a14b0af836d434d33d02f. --- packages/mix/example/README.md | 23 -- packages/mix/example/lib/main.dart | 237 --------------------- packages/mix/example/pubspec.yaml | 31 --- packages/mix/example/test/widget_test.dart | 29 --- 4 files changed, 320 deletions(-) delete mode 100644 packages/mix/example/README.md delete mode 100644 packages/mix/example/lib/main.dart delete mode 100644 packages/mix/example/pubspec.yaml delete mode 100644 packages/mix/example/test/widget_test.dart diff --git a/packages/mix/example/README.md b/packages/mix/example/README.md deleted file mode 100644 index 50a0074df5..0000000000 --- a/packages/mix/example/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# mix_example - -A small demo app for Mix shadow styling. - -It shows the two ways to apply shadow lists with the styler shadow methods -(`BoxStyler.boxShadows` / `BoxStyler.shadows` / `TextStyler.shadows`): - -1. **Literal lists** — pass a `List` / `List` directly. -2. **Design tokens** — define a `BoxShadowToken` / `ShadowToken`, bind its value - once in a `MixScope`, and pass `token.mix()` (a `BoxShadowListMix` / - `ShadowListMix` reference) straight into the same method. - -See [`lib/main.dart`](lib/main.dart). - -## Run - -```bash -flutter run # from packages/mix/example -flutter test # widget smoke test -``` - -The in-repo `mix` / `mix_annotations` packages are wired via `dependency_overrides` -in `pubspec.yaml`, so the example builds without `melos bootstrap`. diff --git a/packages/mix/example/lib/main.dart b/packages/mix/example/lib/main.dart deleted file mode 100644 index 6143174297..0000000000 --- a/packages/mix/example/lib/main.dart +++ /dev/null @@ -1,237 +0,0 @@ -// ABOUTME: Demo app for Mix shadow styling. -// ABOUTME: Shows BoxShadowListMix / ShadowListMix via literal lists and via -// design tokens resolved with `token.mix()`. -import 'package:flutter/material.dart'; -import 'package:mix/mix.dart'; - -void main() => runApp(const ShadowExampleApp()); - -// ----------------------------------------------------------------------------- -// Design tokens. -// -// A `BoxShadowToken` / `ShadowToken` is a named handle for a list of shadows. -// `token.mix()` returns a Mix-compatible reference (a `BoxShadowListMix` / -// `ShadowListMix`) that can be passed straight into the styler shadow methods. -// The actual values are supplied once, at the top of the tree, via `MixScope`. -// ----------------------------------------------------------------------------- -const cardShadow = BoxShadowToken('shadow.card'); -const floatingShadow = BoxShadowToken('shadow.floating'); -const headlineGlow = ShadowToken('shadow.headline-glow'); - -class ShadowExampleApp extends StatelessWidget { - const ShadowExampleApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Mix Shadow Example', - debugShowCheckedModeBanner: false, - home: MixScope( - // Bind each token to its resolved value. Every `token.mix()` used below - // resolves through this scope. - boxShadows: { - cardShadow: const [ - BoxShadow( - color: Color(0x1F000000), - offset: Offset(0, 2), - blurRadius: 8, - ), - BoxShadow( - color: Color(0x14000000), - offset: Offset(0, 1), - blurRadius: 3, - ), - ], - floatingShadow: const [ - BoxShadow( - color: Color(0x33000000), - offset: Offset(0, 12), - blurRadius: 28, - spreadRadius: -6, - ), - ], - }, - shadows: { - headlineGlow: const [ - Shadow(color: Color(0xAA4527A0), offset: Offset(0, 0), blurRadius: 18), - ], - }, - child: const ShadowGallery(), - ), - ); - } -} - -class ShadowGallery extends StatelessWidget { - const ShadowGallery({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: const Color(0xFFF3F4F6), - appBar: AppBar(title: const Text('Mix · Shadow styling')), - body: ListView( - padding: const EdgeInsets.all(24), - children: const [ - _SectionLabel('1 · Literal box shadows (List)'), - _LiteralBoxShadowCard(), - SizedBox(height: 32), - - _SectionLabel('2 · Box shadow token via cardShadow.mix()'), - _TokenBoxShadowCard(), - SizedBox(height: 32), - - _SectionLabel('3 · Elevated token via floatingShadow.mix()'), - _FloatingTokenCard(), - SizedBox(height: 32), - - _SectionLabel('4 · Text shadows (literal + headlineGlow.mix())'), - _TextShadowSample(), - ], - ), - ); - } -} - -/// A box styled with a literal list of [BoxShadowMix] passed to `boxShadows`. -/// -/// Internally Mix wraps this list in a [BoxShadowListMix]; you can also pass an -/// explicit one (e.g. from a token) — see [_TokenBoxShadowCard]. -class _LiteralBoxShadowCard extends StatelessWidget { - const _LiteralBoxShadowCard(); - - @override - Widget build(BuildContext context) { - final style = BoxStyler() - .color(Colors.white) - .height(96) - .borderRounded(16) - .boxShadows([ - BoxShadowMix( - color: Colors.black.withValues(alpha: 0.18), - offset: const Offset(0, 6), - blurRadius: 16, - ), - BoxShadowMix( - color: Colors.indigo.withValues(alpha: 0.20), - offset: const Offset(0, 2), - blurRadius: 4, - ), - ]); - - return Box(style: style, child: const _CardLabel('boxShadows([...])')); - } -} - -/// A box whose shadow comes from the [cardShadow] design token. -/// -/// `cardShadow.mix()` returns a `BoxShadowListMix` reference; the styler keeps -/// its plain `List` signature, so the token flows straight in and -/// resolves against the value registered in [MixScope]. -class _TokenBoxShadowCard extends StatelessWidget { - const _TokenBoxShadowCard(); - - @override - Widget build(BuildContext context) { - final style = BoxStyler() - .color(Colors.white) - .height(96) - .borderRounded(16) - .boxShadows(cardShadow.mix()); - - return Box(style: style, child: const _CardLabel('boxShadows(cardShadow.mix())')); - } -} - -/// Same token mechanism, with a more pronounced "floating" elevation token. -class _FloatingTokenCard extends StatelessWidget { - const _FloatingTokenCard(); - - @override - Widget build(BuildContext context) { - final style = BoxStyler() - .color(Colors.white) - .height(96) - .borderRounded(20) - // `shadows` is the DecorationStyleMixin alias for `boxShadows`. - .shadows(floatingShadow.mix()); - - return Box(style: style, child: const _CardLabel('shadows(floatingShadow.mix())')); - } -} - -/// Text using a literal [ShadowMix] list and a token-backed glow. -class _TextShadowSample extends StatelessWidget { - const _TextShadowSample(); - - @override - Widget build(BuildContext context) { - final literal = TextStyler() - .fontSize(28) - .fontWeight(FontWeight.w800) - .color(Colors.black87) - .shadows([ - ShadowMix( - color: Colors.black.withValues(alpha: 0.35), - offset: const Offset(2, 2), - blurRadius: 4, - ), - ]); - - final glow = TextStyler() - .fontSize(28) - .fontWeight(FontWeight.w800) - .color(Colors.deepPurple) - .shadows(headlineGlow.mix()); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StyledText('Literal shadow', style: literal), - const SizedBox(height: 16), - StyledText('Token glow', style: glow), - ], - ); - } -} - -class _SectionLabel extends StatelessWidget { - const _SectionLabel(this.text); - - final String text; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 12), - child: Text( - text, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: Color(0xFF6B7280), - ), - ), - ); - } -} - -class _CardLabel extends StatelessWidget { - const _CardLabel(this.code); - - final String code; - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - code, - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 13, - color: Color(0xFF374151), - ), - ), - ); - } -} diff --git a/packages/mix/example/pubspec.yaml b/packages/mix/example/pubspec.yaml deleted file mode 100644 index 69411972e8..0000000000 --- a/packages/mix/example/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: mix_example -description: "Demo app showcasing Mix shadow styling: BoxShadowListMix, ShadowListMix, and shadow tokens through .mix()." -publish_to: "none" -version: 0.1.0+1 - -environment: - sdk: ">=3.11.0 <4.0.0" - flutter: ">=3.41.0" - -dependencies: - flutter: - sdk: flutter - mix: - path: .. - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^6.0.0 - -# Resolve the in-repo packages by path so the example builds standalone, -# without requiring `melos bootstrap`. Melos also manages these via the -# (git-ignored) pubspec_overrides.yaml, which takes precedence when present. -dependency_overrides: - mix: - path: .. - mix_annotations: - path: ../../mix_annotations - -flutter: - uses-material-design: true diff --git a/packages/mix/example/test/widget_test.dart b/packages/mix/example/test/widget_test.dart deleted file mode 100644 index 466448aefd..0000000000 --- a/packages/mix/example/test/widget_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mix/mix.dart'; -import 'package:mix_example/main.dart'; - -void main() { - testWidgets('shadow gallery renders and resolves token shadows', ( - tester, - ) async { - await tester.pumpWidget(const ShadowExampleApp()); - await tester.pumpAndSettle(); - - // Section labels are present. - expect(find.textContaining('Literal box shadows'), findsOneWidget); - expect(find.textContaining('cardShadow.mix()'), findsWidgets); - - // The token-backed box resolves to a real BoxDecoration with the shadows - // registered in MixScope (2 box shadows for cardShadow). - final decorated = tester.widget( - find.descendant( - of: find.byType(Box), - matching: find.byType(Container), - ).first, - ); - final decoration = decorated.decoration as BoxDecoration; - expect(decoration.boxShadow, isNotNull); - expect(decoration.boxShadow!.isNotEmpty, isTrue); - }); -} From 73fec4780af3237d2faa9dbe4605ee321822902f Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Jun 2026 00:31:41 -0300 Subject: [PATCH 5/8] Remove shadow token .mix() helpers and handling Remove the deprecated .mix() helpers for ShadowToken and BoxShadowToken and related routing logic. Deleted Prop imports and the special-case branches in DecorationStyleMixin, ShadowStyleMixin, and TextStyleMixin that handled BoxShadowListMix/ShadowListMix token refs, simplifying shadow handling to use the standard call() refs. Update tests to remove assertions and integration tests that relied on the removed .mix() behavior. This cleans up token API surface and test coverage to match the simplified token reference model. --- .../style/mixins/decoration_style_mixin.dart | 5 - .../src/style/mixins/shadow_style_mixin.dart | 5 - .../src/style/mixins/text_style_mixin.dart | 5 - .../lib/src/theme/tokens/value_tokens.dart | 6 - .../shadow_list_token_integration_test.dart | 128 ------------------ .../text_style_token_integration_test.dart | 53 -------- 6 files changed, 202 deletions(-) diff --git a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart index f56da1a2a8..225c2167e1 100644 --- a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; -import '../../core/prop.dart'; import '../../properties/painting/border_mix.dart'; import '../../properties/painting/border_radius_mix.dart'; import '../../properties/painting/decoration_image_mix.dart'; @@ -47,10 +46,6 @@ mixin DecorationStyleMixin> { /// [BoxShadowListMix]); it is routed as a Mix so its token source is /// preserved. A literal list is wrapped as usual. T shadows(List value) { - if (value case final BoxShadowListMix ref) { - return decoration(BoxDecorationMix.create(boxShadow: Prop.mix(ref))); - } - return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart index f92329f166..43a88a9320 100644 --- a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../core/mix_element.dart'; -import '../../core/prop.dart'; import '../../properties/painting/decoration_mix.dart'; import '../../properties/painting/shadow_mix.dart'; @@ -34,10 +33,6 @@ mixin ShadowStyleMixin> { /// [BoxShadowListMix]); it is routed as a Mix so its token source is /// preserved. A literal list is wrapped as usual. T boxShadows(List value) { - if (value case final BoxShadowListMix ref) { - return decoration(BoxDecorationMix.create(boxShadow: Prop.mix(ref))); - } - return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/text_style_mixin.dart b/packages/mix/lib/src/style/mixins/text_style_mixin.dart index 12747c31b0..ec717bc9dd 100644 --- a/packages/mix/lib/src/style/mixins/text_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/text_style_mixin.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; -import '../../core/prop.dart'; import '../../properties/painting/shadow_mix.dart'; import '../../properties/typography/text_style_mix.dart'; @@ -92,10 +91,6 @@ mixin TextStyleMixin> { /// it is routed as a Mix so its token source is preserved. A literal list is /// wrapped as usual. T shadows(List value) { - if (value case final ShadowListMix ref) { - return style(TextStyleMix.create(shadows: Prop.mix(ref))); - } - return style(TextStyleMix.shadows(value)); } diff --git a/packages/mix/lib/src/theme/tokens/value_tokens.dart b/packages/mix/lib/src/theme/tokens/value_tokens.dart index a8ea15fe39..2d92666aa2 100644 --- a/packages/mix/lib/src/theme/tokens/value_tokens.dart +++ b/packages/mix/lib/src/theme/tokens/value_tokens.dart @@ -102,9 +102,6 @@ class BorderSideToken extends MixToken { class ShadowToken extends MixToken> { const ShadowToken(super.name); - /// Returns a Mix framework compatible reference for use with Mix styling utilities. - ShadowListMixRef mix() => .new(Prop.token(this)); - @override ShadowListRef call() => .new(Prop.token(this)); } @@ -113,9 +110,6 @@ class ShadowToken extends MixToken> { class BoxShadowToken extends MixToken> { const BoxShadowToken(super.name); - /// Returns a Mix framework compatible reference for use with Mix styling utilities. - BoxShadowListMixRef mix() => .new(Prop.token(this)); - @override BoxShadowListRef call() => .new(Prop.token(this)); } 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 b7e38a42e2..7d4283c46f 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,132 +128,4 @@ 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(); - }, - ), - ), - ); - }, - ); - }); } diff --git a/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart b/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart index a0c1f74caf..e7acf42e22 100644 --- a/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart +++ b/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart @@ -135,48 +135,6 @@ void main() { }); }); - group('Other Token Types mix() Methods', () { - test('ShadowToken.mix() returns ShadowListMixRef', () { - final token = ShadowToken('test-shadow'); - final mixRef = token.mix(); - - expect(mixRef, isA()); - expect(mixRef, isA()); - expect(mixRef, isA>>()); - expect(mixRef, PropMatcher.isToken(token)); - }); - - test('BoxShadowToken.mix() returns BoxShadowListMixRef', () { - final token = BoxShadowToken('test-boxshadow'); - final mixRef = token.mix(); - - expect(mixRef, isA()); - expect(mixRef, isA()); - expect(mixRef, isA>>()); - expect(mixRef, PropMatcher.isToken(token)); - }); - - test('ShadowToken.mix() returns ShadowListMixRef', () { - final token = ShadowToken('test-shadows'); - final mixRef = token.mix(); - - expect(mixRef, isA()); - expect(mixRef, isA()); - expect(mixRef, isA>>()); - expect(mixRef, PropMatcher.isToken(token)); - }); - - test('BoxShadowToken.mix() returns BoxShadowListMixRef', () { - final token = BoxShadowToken('test-boxshadows'); - final mixRef = token.mix(); - - expect(mixRef, isA()); - expect(mixRef, isA()); - expect(mixRef, isA>>()); - expect(mixRef, PropMatcher.isToken(token)); - }); - }); - group('MixRef Classes', () { test('TextStyleMixRef can be created with a token prop', () { final token = TextStyleToken('test-style'); @@ -243,17 +201,10 @@ void main() { group('Token Registry Integration', () { test('token registry works correctly for all mix() refs', () { final textToken = TextStyleToken('test-style'); - final shadowToken = ShadowToken('test-shadow'); - final boxShadowToken = BoxShadowToken('test-boxshadow'); - final textMixRef = textToken.mix(); - final shadowMixRef = shadowToken.mix(); - final boxShadowMixRef = boxShadowToken.mix(); // All should be detected as token references expect(isAnyTokenRef(textMixRef), isTrue); - expect(isAnyTokenRef(shadowMixRef), isTrue); - expect(isAnyTokenRef(boxShadowMixRef), isTrue); }); test('mix() and call() refs are both detected as token references', () { @@ -380,18 +331,14 @@ void main() { TextStyleMixRef textMixRef = textToken.mix(); ShadowListRef shadowRef = shadowToken.call(); - ShadowListMixRef shadowMixRef = shadowToken.mix(); BoxShadowListRef boxShadowRef = boxShadowToken.call(); - BoxShadowListMixRef boxShadowMixRef = boxShadowToken.mix(); // Runtime verification expect(textRef, isA()); expect(textMixRef, isA()); expect(shadowRef, isA>()); - expect(shadowMixRef, isA()); expect(boxShadowRef, isA>()); - expect(boxShadowMixRef, isA()); }); test('mix() vs call() return different but compatible types', () { From ddc7fa536b70f646080941a9909fdec1baf7b27b Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Jun 2026 09:41:41 -0300 Subject: [PATCH 6/8] Add .mix() for shadow & boxShadow tokens Introduce mix() helpers for ShadowToken and BoxShadowToken that return Mix-compatible refs and preserve Prop.token provenance. Update BoxDecorationMix and TextStyleMix to pass BoxShadowListMix/ShadowListMix through to Prop.mix when already a Mix so token sources aren't wrapped away. Clarify behavior in related style mixin comments. Add/extend tests to cover .mix() return types, styler acceptance, MixScope resolution, and token registry integration. --- .../properties/painting/decoration_mix.dart | 9 +- .../properties/typography/text_style_mix.dart | 11 +- .../style/mixins/decoration_style_mixin.dart | 4 +- .../src/style/mixins/shadow_style_mixin.dart | 4 +- .../src/style/mixins/text_style_mixin.dart | 4 +- .../lib/src/theme/tokens/value_tokens.dart | 6 + .../shadow_list_token_integration_test.dart | 128 ++++++++++++++++++ .../text_style_token_integration_test.dart | 53 ++++++++ 8 files changed, 211 insertions(+), 8 deletions(-) diff --git a/packages/mix/lib/src/properties/painting/decoration_mix.dart b/packages/mix/lib/src/properties/painting/decoration_mix.dart index b3ca09d6f0..04176f12f4 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 33de0e3674..e8d28503ef 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/style/mixins/decoration_style_mixin.dart b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart index 225c2167e1..d9425d41f0 100644 --- a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart @@ -43,8 +43,8 @@ mixin DecorationStyleMixin> { /// /// A design-token reference (`boxShadowToken.mix()`) arrives here as a /// [BoxShadowListMix] (it implements both `List` and - /// [BoxShadowListMix]); it is routed as a Mix so its token source is - /// preserved. A literal list is wrapped as usual. + /// [BoxShadowListMix]); [BoxDecorationMix] passes it through to [Prop.mix] + /// unwrapped so its token source is preserved. A literal list is wrapped. T shadows(List value) { return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart index 43a88a9320..df9aa797c1 100644 --- a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart @@ -30,8 +30,8 @@ mixin ShadowStyleMixin> { /// /// A design-token reference (`boxShadowToken.mix()`) arrives here as a /// [BoxShadowListMix] (it implements both `List` and - /// [BoxShadowListMix]); it is routed as a Mix so its token source is - /// preserved. A literal list is wrapped as usual. + /// [BoxShadowListMix]); [BoxDecorationMix] passes it through to [Prop.mix] + /// unwrapped so its token source is preserved. A literal list is wrapped. T boxShadows(List value) { return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/text_style_mixin.dart b/packages/mix/lib/src/style/mixins/text_style_mixin.dart index ec717bc9dd..ae8a8b797d 100644 --- a/packages/mix/lib/src/style/mixins/text_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/text_style_mixin.dart @@ -88,8 +88,8 @@ mixin TextStyleMixin> { /// /// A design-token reference (`shadowToken.mix()`) arrives here as a /// [ShadowListMix] (it implements both `List` and [ShadowListMix]); - /// it is routed as a Mix so its token source is preserved. A literal list is - /// wrapped as usual. + /// [TextStyleMix] passes it through to [Prop.mix] unwrapped so its token + /// source is preserved. A literal list is wrapped as usual. T shadows(List value) { return style(TextStyleMix.shadows(value)); } diff --git a/packages/mix/lib/src/theme/tokens/value_tokens.dart b/packages/mix/lib/src/theme/tokens/value_tokens.dart index 2d92666aa2..a8ea15fe39 100644 --- a/packages/mix/lib/src/theme/tokens/value_tokens.dart +++ b/packages/mix/lib/src/theme/tokens/value_tokens.dart @@ -102,6 +102,9 @@ class BorderSideToken extends MixToken { class ShadowToken extends MixToken> { const ShadowToken(super.name); + /// Returns a Mix framework compatible reference for use with Mix styling utilities. + ShadowListMixRef mix() => .new(Prop.token(this)); + @override ShadowListRef call() => .new(Prop.token(this)); } @@ -110,6 +113,9 @@ class ShadowToken extends MixToken> { class BoxShadowToken extends MixToken> { const BoxShadowToken(super.name); + /// Returns a Mix framework compatible reference for use with Mix styling utilities. + BoxShadowListMixRef mix() => .new(Prop.token(this)); + @override BoxShadowListRef call() => .new(Prop.token(this)); } 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 7d4283c46f..b7e38a42e2 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(); + }, + ), + ), + ); + }, + ); + }); } diff --git a/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart b/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart index e7acf42e22..a0c1f74caf 100644 --- a/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart +++ b/packages/mix/test/src/theme/tokens/text_style_token_integration_test.dart @@ -135,6 +135,48 @@ void main() { }); }); + group('Other Token Types mix() Methods', () { + test('ShadowToken.mix() returns ShadowListMixRef', () { + final token = ShadowToken('test-shadow'); + final mixRef = token.mix(); + + expect(mixRef, isA()); + expect(mixRef, isA()); + expect(mixRef, isA>>()); + expect(mixRef, PropMatcher.isToken(token)); + }); + + test('BoxShadowToken.mix() returns BoxShadowListMixRef', () { + final token = BoxShadowToken('test-boxshadow'); + final mixRef = token.mix(); + + expect(mixRef, isA()); + expect(mixRef, isA()); + expect(mixRef, isA>>()); + expect(mixRef, PropMatcher.isToken(token)); + }); + + test('ShadowToken.mix() returns ShadowListMixRef', () { + final token = ShadowToken('test-shadows'); + final mixRef = token.mix(); + + expect(mixRef, isA()); + expect(mixRef, isA()); + expect(mixRef, isA>>()); + expect(mixRef, PropMatcher.isToken(token)); + }); + + test('BoxShadowToken.mix() returns BoxShadowListMixRef', () { + final token = BoxShadowToken('test-boxshadows'); + final mixRef = token.mix(); + + expect(mixRef, isA()); + expect(mixRef, isA()); + expect(mixRef, isA>>()); + expect(mixRef, PropMatcher.isToken(token)); + }); + }); + group('MixRef Classes', () { test('TextStyleMixRef can be created with a token prop', () { final token = TextStyleToken('test-style'); @@ -201,10 +243,17 @@ void main() { group('Token Registry Integration', () { test('token registry works correctly for all mix() refs', () { final textToken = TextStyleToken('test-style'); + final shadowToken = ShadowToken('test-shadow'); + final boxShadowToken = BoxShadowToken('test-boxshadow'); + final textMixRef = textToken.mix(); + final shadowMixRef = shadowToken.mix(); + final boxShadowMixRef = boxShadowToken.mix(); // All should be detected as token references expect(isAnyTokenRef(textMixRef), isTrue); + expect(isAnyTokenRef(shadowMixRef), isTrue); + expect(isAnyTokenRef(boxShadowMixRef), isTrue); }); test('mix() and call() refs are both detected as token references', () { @@ -331,14 +380,18 @@ void main() { TextStyleMixRef textMixRef = textToken.mix(); ShadowListRef shadowRef = shadowToken.call(); + ShadowListMixRef shadowMixRef = shadowToken.mix(); BoxShadowListRef boxShadowRef = boxShadowToken.call(); + BoxShadowListMixRef boxShadowMixRef = boxShadowToken.mix(); // Runtime verification expect(textRef, isA()); expect(textMixRef, isA()); expect(shadowRef, isA>()); + expect(shadowMixRef, isA()); expect(boxShadowRef, isA>()); + expect(boxShadowMixRef, isA()); }); test('mix() vs call() return different but compatible types', () { From 2d5a068fd8daf5097d0ade3f796517e56a833d97 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Jun 2026 09:54:04 -0300 Subject: [PATCH 7/8] Add Prop import and simplify shadow docs Import '../../core/prop.dart' into decoration, shadow, and text style mixins and shorten the doc comments for shadows/boxShadows methods by removing the verbose token-unwrapping explanation. This keeps the documentation concise and ensures the Prop symbol is available for these mixin files. --- .../mix/lib/src/style/mixins/decoration_style_mixin.dart | 8 ++------ packages/mix/lib/src/style/mixins/shadow_style_mixin.dart | 8 ++------ packages/mix/lib/src/style/mixins/text_style_mixin.dart | 8 ++------ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart index d9425d41f0..e78d50ee5a 100644 --- a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; +import '../../core/prop.dart'; import '../../properties/painting/border_mix.dart'; import '../../properties/painting/border_radius_mix.dart'; import '../../properties/painting/decoration_image_mix.dart'; @@ -39,12 +40,7 @@ mixin DecorationStyleMixin> { return decoration(BoxDecorationMix.boxShadow([value])); } - /// Sets multiple shadows. - /// - /// A design-token reference (`boxShadowToken.mix()`) arrives here as a - /// [BoxShadowListMix] (it implements both `List` and - /// [BoxShadowListMix]); [BoxDecorationMix] passes it through to [Prop.mix] - /// unwrapped so its token source is preserved. A literal list is wrapped. + /// Sets multiple shadows T shadows(List value) { return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart index df9aa797c1..ab67580c92 100644 --- a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/mix_element.dart'; +import '../../core/prop.dart'; import '../../properties/painting/decoration_mix.dart'; import '../../properties/painting/shadow_mix.dart'; @@ -26,12 +27,7 @@ mixin ShadowStyleMixin> { return decoration(BoxDecorationMix.boxShadow([shadow])); } - /// Creates multiple box shadows from a list of BoxShadowMix. - /// - /// A design-token reference (`boxShadowToken.mix()`) arrives here as a - /// [BoxShadowListMix] (it implements both `List` and - /// [BoxShadowListMix]); [BoxDecorationMix] passes it through to [Prop.mix] - /// unwrapped so its token source is preserved. A literal list is wrapped. + /// Creates multiple box shadows from a list of BoxShadowMix T boxShadows(List value) { return decoration(BoxDecorationMix.boxShadow(value)); } diff --git a/packages/mix/lib/src/style/mixins/text_style_mixin.dart b/packages/mix/lib/src/style/mixins/text_style_mixin.dart index ae8a8b797d..c8178c9828 100644 --- a/packages/mix/lib/src/style/mixins/text_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/text_style_mixin.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; +import '../../core/prop.dart'; import '../../properties/painting/shadow_mix.dart'; import '../../properties/typography/text_style_mix.dart'; @@ -84,12 +85,7 @@ mixin TextStyleMixin> { return style(TextStyleMix.fontFamilyFallback(value)); } - /// Sets text shadows. - /// - /// A design-token reference (`shadowToken.mix()`) arrives here as a - /// [ShadowListMix] (it implements both `List` and [ShadowListMix]); - /// [TextStyleMix] passes it through to [Prop.mix] unwrapped so its token - /// source is preserved. A literal list is wrapped as usual. + /// Sets text shadows T shadows(List value) { return style(TextStyleMix.shadows(value)); } From 8b7977fe25df5c492ca5c00efd08a290d5cecce4 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Tue, 23 Jun 2026 09:55:13 -0300 Subject: [PATCH 8/8] Remove unused prop import from mixins Remove the unused import '../../core/prop.dart' from decoration_style_mixin.dart, shadow_style_mixin.dart, and text_style_mixin.dart to clean up imports and eliminate unused-import warnings. No functional changes. --- packages/mix/lib/src/style/mixins/decoration_style_mixin.dart | 1 - packages/mix/lib/src/style/mixins/shadow_style_mixin.dart | 1 - packages/mix/lib/src/style/mixins/text_style_mixin.dart | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart index e78d50ee5a..b362f7d5b5 100644 --- a/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/decoration_style_mixin.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; -import '../../core/prop.dart'; import '../../properties/painting/border_mix.dart'; import '../../properties/painting/border_radius_mix.dart'; import '../../properties/painting/decoration_image_mix.dart'; diff --git a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart index ab67580c92..8d23f136ed 100644 --- a/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/shadow_style_mixin.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../core/mix_element.dart'; -import '../../core/prop.dart'; import '../../properties/painting/decoration_mix.dart'; import '../../properties/painting/shadow_mix.dart'; diff --git a/packages/mix/lib/src/style/mixins/text_style_mixin.dart b/packages/mix/lib/src/style/mixins/text_style_mixin.dart index c8178c9828..f10111dedd 100644 --- a/packages/mix/lib/src/style/mixins/text_style_mixin.dart +++ b/packages/mix/lib/src/style/mixins/text_style_mixin.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import '../../core/mix_element.dart'; -import '../../core/prop.dart'; import '../../properties/painting/shadow_mix.dart'; import '../../properties/typography/text_style_mix.dart';