Skip to content

Commit 09a4a7d

Browse files
feat: example redesign (#287)
* feat(motor): redesign example app with Dia-inspired monochrome system Rework the Motor example into a near-monochrome, Dia-inspired showcase that demonstrates real product motion rather than abstract demos. - example_design: replace the 6-accent palette with a quiet monochrome system (light + dark), a single soft shadow, generous radii, featherweight type, and a sparing spectrum gradient; add FrostedCard, NeutralButton, GhostPill, AmbientGlow, and TrajectoryLine primitives. Legacy token names are aliased onto the new palette so the stupid_simple_sheet example stays monochrome. - Headline demo: Interruptible Motion graphs spring vs curve continuity with a ported value-recording notifier and trajectory line. - Everyday UI: drawer, snap carousel (FrictionMotion.project), toggle, toast, segmented selector, accordion, and loaders (array of tracks + loop modes). - Reframe showcases: now_playing (replaces the glow panel), staggered entrance (replaces the launch checklist); restyle card stack, title slide, flip card. - Restyle gestures (draggable icons, picture-in-picture, drag reorder) to neutral/frosted; PiP now snaps via FrictionMotion.project. - Rebuild the overview into Continuity / Everyday UI / Compose Motion / Gestures with monochrome previews; cut tap playground, velocity tracking, 2D redirection, and standalone loop modes. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(motor)!: refine track API (optional initial, per-animation from/withVelocity, TrackBuilder) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bd4ad19 commit 09a4a7d

57 files changed

Lines changed: 4775 additions & 3328 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
export 'src/example_card.dart';
2-
export 'src/example_theme.dart';
3-
export 'src/section_header.dart';
1+
export 'package:example_design/src/dia_widgets.dart';
2+
export 'package:example_design/src/example_card.dart';
3+
export 'package:example_design/src/example_theme.dart';
4+
export 'package:example_design/src/section_header.dart';
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import 'dart:ui';
2+
3+
import 'package:example_design/src/example_theme.dart';
4+
import 'package:flutter/cupertino.dart';
5+
6+
/// A frosted, single-shadow surface — the primary content container.
7+
///
8+
/// Uses a translucent fill plus a backdrop blur so content layered beneath
9+
/// (like an [AmbientGlow]) reads as soft, refracted light rather than a hard
10+
/// panel.
11+
class FrostedCard extends StatelessWidget {
12+
const FrostedCard({
13+
required this.child,
14+
this.padding = EdgeInsets.zero,
15+
this.radius = ExampleTheme.surfaceRadius,
16+
this.clip = true,
17+
super.key,
18+
});
19+
20+
final Widget child;
21+
final EdgeInsetsGeometry padding;
22+
final double radius;
23+
final bool clip;
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
final t = ExampleTheme.of(context);
28+
final borderRadius = BorderRadius.circular(radius);
29+
30+
final content = DecoratedBox(
31+
decoration: BoxDecoration(
32+
color: t.surface,
33+
borderRadius: borderRadius,
34+
border: Border.all(color: t.border),
35+
boxShadow: t.softShadow,
36+
),
37+
child: Padding(padding: padding, child: child),
38+
);
39+
40+
if (!clip) return content;
41+
42+
return ClipRRect(
43+
borderRadius: borderRadius,
44+
child: BackdropFilter(
45+
filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24),
46+
child: content,
47+
),
48+
);
49+
}
50+
}
51+
52+
/// A neutral, quiet filled button. Never chromatic.
53+
class NeutralButton extends StatefulWidget {
54+
const NeutralButton({
55+
required this.onPressed,
56+
required this.child,
57+
this.filled = true,
58+
super.key,
59+
});
60+
61+
/// The action. When null, the button reads as disabled.
62+
final VoidCallback? onPressed;
63+
64+
/// When false, renders as a transparent ghost button.
65+
final bool filled;
66+
67+
final Widget child;
68+
69+
@override
70+
State<NeutralButton> createState() => _NeutralButtonState();
71+
}
72+
73+
class _NeutralButtonState extends State<NeutralButton> {
74+
bool _pressed = false;
75+
76+
@override
77+
Widget build(BuildContext context) {
78+
final t = ExampleTheme.of(context);
79+
final enabled = widget.onPressed != null;
80+
final bg = widget.filled
81+
? (_pressed ? t.textPrimary : t.pebble)
82+
: (_pressed ? t.fog : const Color(0x00000000));
83+
final fg = widget.filled && _pressed
84+
? t.surfaceSolid
85+
: enabled
86+
? t.textPrimary
87+
: t.textTertiary;
88+
89+
return GestureDetector(
90+
onTapDown: enabled ? (_) => setState(() => _pressed = true) : null,
91+
onTapUp: enabled ? (_) => setState(() => _pressed = false) : null,
92+
onTapCancel: enabled ? () => setState(() => _pressed = false) : null,
93+
onTap: widget.onPressed,
94+
child: AnimatedContainer(
95+
duration: const Duration(milliseconds: 120),
96+
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 11),
97+
decoration: BoxDecoration(
98+
color: bg,
99+
borderRadius: BorderRadius.circular(999),
100+
),
101+
child: DefaultTextStyle.merge(
102+
style: TextStyle(
103+
color: fg,
104+
fontSize: 14,
105+
fontWeight: FontWeight.w500,
106+
letterSpacing: -0.1,
107+
),
108+
child: IconTheme.merge(
109+
data: IconThemeData(color: fg, size: 18),
110+
child: widget.child,
111+
),
112+
),
113+
),
114+
);
115+
}
116+
}
117+
118+
/// A small, quiet pill / chip label.
119+
class GhostPill extends StatelessWidget {
120+
const GhostPill(this.text, {this.icon, super.key});
121+
122+
final String text;
123+
final IconData? icon;
124+
125+
@override
126+
Widget build(BuildContext context) {
127+
final t = ExampleTheme.of(context);
128+
return DecoratedBox(
129+
decoration: BoxDecoration(
130+
color: t.fog,
131+
borderRadius: BorderRadius.circular(999),
132+
),
133+
child: Padding(
134+
padding: EdgeInsets.fromLTRB(icon != null ? 10 : 14, 7, 14, 7),
135+
child: Row(
136+
mainAxisSize: MainAxisSize.min,
137+
children: [
138+
if (icon != null) ...[
139+
Icon(icon, size: 14, color: t.textSecondary),
140+
const SizedBox(width: 6),
141+
],
142+
Text(
143+
text,
144+
style: TextStyle(
145+
color: t.textSecondary,
146+
fontSize: 12,
147+
fontWeight: FontWeight.w500,
148+
letterSpacing: -0.1,
149+
),
150+
),
151+
],
152+
),
153+
),
154+
);
155+
}
156+
}
157+
158+
/// A soft, blurred wash of the [ExampleTheme.spectrum] gradient — the system's
159+
/// only chromatic moment. Place it behind frosted surfaces for refracted light.
160+
class AmbientGlow extends StatelessWidget {
161+
const AmbientGlow({
162+
this.opacity = 0.35,
163+
this.blur = 60,
164+
this.alignment = Alignment.topCenter,
165+
super.key,
166+
});
167+
168+
final double opacity;
169+
final double blur;
170+
final Alignment alignment;
171+
172+
@override
173+
Widget build(BuildContext context) {
174+
return IgnorePointer(
175+
child: Opacity(
176+
opacity: opacity,
177+
child: ImageFiltered(
178+
imageFilter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
179+
child: Align(
180+
alignment: alignment,
181+
child: FractionallySizedBox(
182+
widthFactor: 0.9,
183+
heightFactor: 0.55,
184+
child: DecoratedBox(
185+
decoration: const BoxDecoration(
186+
gradient: ExampleTheme.spectrum,
187+
borderRadius: BorderRadius.all(Radius.circular(999)),
188+
),
189+
),
190+
),
191+
),
192+
),
193+
),
194+
);
195+
}
196+
}
197+
198+
/// Paints a poly-line through normalized points (0..1 in both axes).
199+
///
200+
/// The most recent samples are at the right edge; older samples fade out to
201+
/// the left, giving a sense of motion history.
202+
class TrajectoryLine extends StatelessWidget {
203+
const TrajectoryLine({
204+
required this.points,
205+
this.color,
206+
this.gradient,
207+
this.thickness = 3,
208+
this.fade = true,
209+
super.key,
210+
});
211+
212+
/// Normalized points where x and y are both in `0..1`.
213+
final List<Offset> points;
214+
final Color? color;
215+
final Gradient? gradient;
216+
final double thickness;
217+
final bool fade;
218+
219+
@override
220+
Widget build(BuildContext context) {
221+
return CustomPaint(
222+
painter: _LinePainter(
223+
points: points,
224+
color: color ?? const Color(0xFF000000),
225+
gradient: gradient,
226+
thickness: thickness,
227+
fade: fade,
228+
),
229+
size: Size.infinite,
230+
);
231+
}
232+
}
233+
234+
class _LinePainter extends CustomPainter {
235+
_LinePainter({
236+
required this.points,
237+
required this.color,
238+
required this.gradient,
239+
required this.thickness,
240+
required this.fade,
241+
});
242+
243+
final List<Offset> points;
244+
final Color color;
245+
final Gradient? gradient;
246+
final double thickness;
247+
final bool fade;
248+
249+
@override
250+
void paint(Canvas canvas, Size size) {
251+
if (points.length < 2) return;
252+
253+
final scaled = [
254+
for (final p in points) Offset(p.dx * size.width, p.dy * size.height),
255+
];
256+
257+
final shader = gradient?.createShader(
258+
Rect.fromLTWH(0, 0, size.width, size.height),
259+
);
260+
261+
for (var i = 0; i < scaled.length - 1; i++) {
262+
final progress = i / (scaled.length - 1);
263+
final opacity = fade ? Curves.easeIn.transform(progress) : 1.0;
264+
final paint = Paint()
265+
..strokeWidth = thickness
266+
..style = PaintingStyle.stroke
267+
..strokeCap = StrokeCap.round
268+
..strokeJoin = StrokeJoin.round;
269+
if (shader != null) {
270+
paint
271+
..shader = shader
272+
..color = const Color(0xFFFFFFFF).withValues(alpha: opacity);
273+
} else {
274+
paint.color = color.withValues(alpha: opacity);
275+
}
276+
canvas.drawLine(scaled[i], scaled[i + 1], paint);
277+
}
278+
}
279+
280+
@override
281+
bool shouldRepaint(_LinePainter oldDelegate) =>
282+
points != oldDelegate.points ||
283+
color != oldDelegate.color ||
284+
gradient != oldDelegate.gradient ||
285+
thickness != oldDelegate.thickness ||
286+
fade != oldDelegate.fade;
287+
}

0 commit comments

Comments
 (0)