Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/motion-and-high-refresh.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ Introduce `lib/core/motion/` with a single source of truth for durations and cur
|-------|-------|-----|
| `instant` | 0 ms | reduced-motion / disabled |
| `fast` | 120 ms | hover, small state changes |
| `standard` | 200 ms | dialogs, menus, expand/collapse |
| `standard` | 200 ms | dialogs, menus, general surfaces |
| `treeExpand` | 200 ms (= `standard`) | connection/SDUI tree: **chevron + height** share one clock (#480) |
| `slow` | 320 ms | emphasized / large surfaces, theme cross-fade |

### 4.2 Curve tokens
Expand All @@ -91,7 +92,8 @@ Introduce `lib/core/motion/` with a single source of truth for durations and cur
|-------|-------|-----|
| `enter` | `easeOutCubic` | elements appearing (decelerate) |
| `exit` | `easeInCubic` | elements leaving (accelerate) |
| `standard` | `easeInOutCubic` | move/resize in place |
| `standardCurve` | `easeInOutCubic` | move/resize in place |
| `treeExpandCurve` | = `enter` | tree expand chevron + `QueryaAnimatedExpand` |
| `emphasized` | `Curves.easeInOutCubicEmphasized` | hero / theme transitions |

### 4.3 Reduced motion / accessibility
Expand Down Expand Up @@ -151,6 +153,9 @@ When reviewing PRs that touch animation:
2. Require Full / Reduced / Off + OS `disableAnimations` coverage for new transitions.
3. Split / resize: no spring or lag mid-drag; settle only on release / focus chrome.
4. Never stagger or fade virtualized result rows while scrolling.
5. Tree expand: chevron `AnimatedRotation` and `QueryaAnimatedExpand` **must** use
`QueryaMotion.treeExpand` + `treeExpandCurve` (not `fast`/`standardCurve` mixed
with `standard`/`enter`).

**Allowed named non-token durations** (named + documented — not magic literals at call sites):

Expand Down
4 changes: 2 additions & 2 deletions lib/core/motion/querya_animated_expand.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class QueryaAnimatedExpand extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: context.motionDuration(QueryaMotion.standard),
curve: context.motionCurve(QueryaMotion.enter),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
alignment: alignment,
clipBehavior: Clip.hardEdge,
child:
Expand Down
9 changes: 8 additions & 1 deletion lib/core/motion/querya_motion.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ abstract final class QueryaMotion {
/// Hover, small state changes.
static const Duration fast = Duration(milliseconds: 120);

/// Dialogs, menus, expand/collapse.
/// Dialogs, menus, general surface transitions.
static const Duration standard = Duration(milliseconds: 200);

/// Emphasized transitions (theme cross-fade, large surfaces).
static const Duration slow = Duration(milliseconds: 320);

/// Connection / SDUI tree expand: chevron rotation **and** height morph share
/// this duration so one gesture does not finish on two clocks (#480).
static const Duration treeExpand = standard;

/// Elements appearing (decelerate).
static const Curve enter = Curves.easeOutCubic;

Expand All @@ -32,6 +36,9 @@ abstract final class QueryaMotion {
/// Hero / theme transitions.
static const Curve emphasized = Curves.easeInOutCubicEmphasized;

/// Curve for [treeExpand] (chevron + [QueryaAnimatedExpand] height).
static const Curve treeExpandCurve = enter;

/// Returns [token] adjusted for accessibility and [QueryaMotionScope] level.
static Duration effectiveDuration(BuildContext context, Duration token) {
if (token == instant) return instant;
Expand Down
12 changes: 6 additions & 6 deletions lib/core/sdui/sdui_tree_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import 'package:querya_desktop/shared/widgets/widgets.dart';
///
/// Visible rows are flattened into a [ListView.builder] so only viewport
/// rows are built (large schemas no longer create a full widget Column).
/// Expand chevrons use [QueryaMotion] tokens (same dialect as native trees).
/// Height morph via [QueryaAnimatedExpand] is not used here: nested expand
/// widgets conflict with the flat virtualized row list (see issue #480 for
/// coordinated expand timing across trees).
/// Expand chevrons and height morph share [QueryaMotion.treeExpand] /
/// [QueryaMotion.treeExpandCurve]. Height morph via [QueryaAnimatedExpand] is
/// not used on the flat virtualized row list (nested expand would fight
/// `ListView` itemExtent); chevron timing still matches native trees.
class SduiTreeBuilder extends material.StatefulWidget {
const SduiTreeBuilder({
super.key,
Expand Down Expand Up @@ -233,8 +233,8 @@ class SduiTreeBuilderState extends material.State<SduiTreeBuilder> {
padding: const material.EdgeInsets.all(2),
child: material.AnimatedRotation(
turns: isExpanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down
4 changes: 2 additions & 2 deletions lib/features/connections/connections_panel_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ class _ExtensionConnectionTileState extends State<_ExtensionConnectionTile> {
padding: const material.EdgeInsets.all(2),
child: material.AnimatedRotation(
turns: widget.isExpanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: 16,
Expand Down
4 changes: 2 additions & 2 deletions lib/features/connections/connections_panel_mongo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ class _MongoConnectionTileState extends State<_MongoConnectionTile> {
padding: const material.EdgeInsets.all(2),
child: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
material.Icons.chevron_right_rounded,
size: 16,
Expand Down
12 changes: 6 additions & 6 deletions lib/features/connections/connections_panel_mysql.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ class _MysqlConnectionTileState extends State<_MysqlConnectionTile> {
padding: const material.EdgeInsets.all(2),
child: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: 16,
Expand Down Expand Up @@ -440,8 +440,8 @@ class _MysqlDatabaseNodeState extends State<_MysqlDatabaseNode> {
label: widget.databaseName,
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down Expand Up @@ -632,8 +632,8 @@ class _MysqlObjectGroupState extends State<_MysqlObjectGroup> {
label: '${widget.label} (${widget.items.length})',
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down
20 changes: 10 additions & 10 deletions lib/features/connections/connections_panel_pg_tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ class _PgDatabasesNodeState extends State<_PgDatabasesNode> {
label: 'Databases (${widget.databases.length})',
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down Expand Up @@ -340,8 +340,8 @@ class _PgDatabaseNodeState extends State<_PgDatabaseNode> {
label: widget.databaseName,
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down Expand Up @@ -535,8 +535,8 @@ class _PgSchemasNodeState extends State<_PgSchemasNode> {
label: 'Schemas (${widget.schemas.length})',
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down Expand Up @@ -691,8 +691,8 @@ class _PgSchemaNodeState extends State<_PgSchemaNode> {
label: widget.schemaName,
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down Expand Up @@ -1009,8 +1009,8 @@ class _PgObjectGroupState extends State<_PgObjectGroup> {
label: '${widget.label} (${widget.items.length})',
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ class _PostgresConnectionTileState extends State<_PostgresConnectionTile> {
padding: const material.EdgeInsets.all(2),
child: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
material.Icons.chevron_right_rounded,
size: 16,
Expand Down
4 changes: 2 additions & 2 deletions lib/features/connections/connections_panel_redis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ class _RedisConnectionTileState extends State<_RedisConnectionTile> {
padding: const material.EdgeInsets.all(2),
child: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
material.Icons.chevron_right_rounded,
size: 16,
Expand Down
4 changes: 2 additions & 2 deletions lib/features/connections/connections_panel_sidebar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ class _FolderTileState extends State<_FolderTile> {
children: [
material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
material.Icons.chevron_right_rounded,
size: 18,
Expand Down
8 changes: 4 additions & 4 deletions lib/features/connections/connections_panel_sqlite.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ class _SqliteConnectionTileState extends State<_SqliteConnectionTile> {
padding: const material.EdgeInsets.all(2),
child: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: 16,
Expand Down Expand Up @@ -362,8 +362,8 @@ class _SqliteObjectGroupState extends State<_SqliteObjectGroup> {
label: '${widget.label} (${widget.items.length})',
leading: material.AnimatedRotation(
turns: _expanded ? 0.25 : 0,
duration: context.motionDuration(QueryaMotion.fast),
curve: context.motionCurve(QueryaMotion.standardCurve),
duration: context.motionDuration(QueryaMotion.treeExpand),
curve: context.motionCurve(QueryaMotion.treeExpandCurve),
child: material.Icon(
QueryaIcons.expandClosed,
size: QueryaIconSizes.treeExpand,
Expand Down
13 changes: 13 additions & 0 deletions test/core/motion/querya_animated_expand_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/motion/querya_animated_expand.dart';
import 'package:querya_desktop/core/motion/querya_motion.dart';

void main() {
testWidgets('QueryaAnimatedExpand hides child when collapsed',
Expand All @@ -23,6 +24,18 @@ void main() {

expect(find.text('child'), findsOneWidget);
});

testWidgets('uses treeExpand duration/curve tokens', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: _ExpandHost(expanded: true),
),
);

final size = tester.widget<AnimatedSize>(find.byType(AnimatedSize));
expect(size.duration, QueryaMotion.treeExpand);
expect(size.curve, QueryaMotion.treeExpandCurve);
});
}

class _ExpandHost extends StatelessWidget {
Expand Down
2 changes: 2 additions & 0 deletions test/core/motion/querya_motion_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ void main() {
expect(QueryaMotion.fast, const Duration(milliseconds: 120));
expect(QueryaMotion.standard, const Duration(milliseconds: 200));
expect(QueryaMotion.slow, const Duration(milliseconds: 320));
expect(QueryaMotion.treeExpand, QueryaMotion.standard);
});

test('curve constants are set', () {
expect(QueryaMotion.enter, Curves.easeOutCubic);
expect(QueryaMotion.exit, Curves.easeInCubic);
expect(QueryaMotion.standardCurve, Curves.easeInOutCubic);
expect(QueryaMotion.emphasized, Curves.easeInOutCubicEmphasized);
expect(QueryaMotion.treeExpandCurve, QueryaMotion.enter);
});
});

Expand Down
Loading