Files
social-app/docs/plans/2026-03-13-home-screen-visual-refresh.md
T
qzl 3273d63b23 feat: 重构 Home Screen 视觉设计与消息输入组件
- 新增 Home Screen 视觉设计 token (背景、工具栏、对话区、输入框等)
- 重构首页布局为浮动式底部输入栈结构
- 新增 HomeBackgroundField、HomeFloatingHeader、HomeAttachmentStrip 组件
- 优化 MessageComposer 视觉样式为悬浮 shell 设计
- 添加相关测试用例
2026-03-13 17:25:29 +08:00

16 KiB

Home Screen Visual Refresh Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Rebuild the post-login home screen into a calm, premium assistant homepage with a layered background field, a clearer conversation stage, and a floating input island that carries text, voice, and attachment states without relying on helper copy.

Architecture: Keep the existing chat data flow and AG-UI behavior unchanged. Refactor the screen into a surface-based composition: background field at the bottom, content layer for header and conversation stage in the middle, and a floating input layer on top. Move visual semantics into shared design tokens first, then rebuild MessageComposer and HomeScreen around those tokens.

Tech Stack: Flutter, Material, flutter_bloc, existing design token system, existing widget tests in apps/test/features/home/ui/widgets/.


Task 1: Add home surface tokens

Files:

  • Modify: apps/lib/core/theme/design_tokens.dart
  • Reference: apps/rules/visual_design_language.md

Step 1: Write the failing test

Add a widget test assertion that depends on a new home token being used by MessageComposer, for example:

testWidgets('composer uses refreshed home surface token', (tester) async {
  await tester.pumpWidget(_buildTestApp(
    mode: MessageComposerMode.text,
    process: MessageComposerProcess.idle,
    hasMessage: false,
    isWaitingAgent: false,
  ));

  final container = tester.widget<Container>(
    find.byKey(messageComposerContainerKey),
  );
  final decoration = container.decoration! as BoxDecoration;
  expect(decoration.color, AppColors.homeComposerShell);
});

Step 2: Run test to verify it fails

Run: flutter test apps/test/features/home/ui/widgets/home_composer_test.dart

Expected: FAIL because homeComposerShell does not exist yet.

Step 3: Write minimal implementation

Add the first batch of home-specific tokens in apps/lib/core/theme/design_tokens.dart, keeping names semantic instead of layout-specific:

static const homeBackgroundTop = Color(0xFFF5F9FF);
static const homeBackgroundBottom = Color(0xFFF7FAFE);
static const homeBackgroundGlow = Color(0xFFDCEBFF);
static const homeBackgroundGlowSoft = Color(0xFFF1F6FF);
static const homeToolbarSurface = Color(0xF2FFFFFF);
static const homeToolbarBorder = Color(0xD9E6F7);
static const homeConversationSurface = Color(0xBFFFFFFF);
static const homeConversationBorder = Color(0xDDE8F6);
static const homeComposerShell = Color(0xFDFCFEFF);
static const homeComposerInner = Color(0xFFF7FAFE);
static const homeComposerBorder = Color(0xD7E3F3);
static const homeComposerAccent = Color(0xFFEAF3FF);
static const homeAttachmentSurface = Color(0xFFF3F7FD);

Step 4: Run test to verify it passes

Run: flutter test apps/test/features/home/ui/widgets/home_composer_test.dart

Expected: PASS, with the new token available to downstream widgets.

Step 5: Commit

git add apps/lib/core/theme/design_tokens.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "feat: add home screen surface tokens"

Task 2: Extract background field and floating header primitives

Files:

  • Create: apps/lib/features/home/ui/widgets/home_background_field.dart
  • Create: apps/lib/features/home/ui/widgets/home_floating_header.dart
  • Modify: apps/lib/features/home/ui/screens/home_screen.dart

Step 1: Write the failing test

Create a focused widget test for the new background field composition:

testWidgets('home background field renders layered glow surfaces', (tester) async {
  await tester.pumpWidget(const MaterialApp(
    home: Scaffold(body: HomeBackgroundField()),
  ));

  expect(find.byKey(homeBackgroundFieldKey), findsOneWidget);
  expect(find.byKey(homeTopGlowKey), findsOneWidget);
  expect(find.byKey(homeBottomGlowKey), findsOneWidget);
});

Step 2: Run test to verify it fails

Run: flutter test apps/test/features/home/ui/widgets/home_background_field_test.dart

Expected: FAIL because the widget file and keys do not exist.

Step 3: Write minimal implementation

Build a reusable background widget using only tokens and soft gradients:

class HomeBackgroundField extends StatelessWidget {
  const HomeBackgroundField({super.key});

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      key: homeBackgroundFieldKey,
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [
            AppColors.homeBackgroundTop,
            AppColors.homeBackgroundBottom,
          ],
        ),
      ),
      child: Stack(
        children: const [
          _TopGlow(),
          _BottomGlow(),
        ],
      ),
    );
  }
}

Add a matching lightweight floating header widget so HomeScreen stops inlining all visual treatment in one file.

Step 4: Run test to verify it passes

Run: flutter test apps/test/features/home/ui/widgets/home_background_field_test.dart

Expected: PASS.

Step 5: Commit

git add apps/lib/features/home/ui/widgets/home_background_field.dart apps/lib/features/home/ui/widgets/home_floating_header.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_background_field_test.dart
git commit -m "feat: add layered home screen background primitives"

Task 3: Rebuild MessageComposer as a floating input island

Files:

  • Modify: apps/lib/shared/widgets/message_composer.dart
  • Modify: apps/test/features/home/ui/widgets/home_composer_test.dart

Step 1: Write the failing test

Extend the existing composer tests to enforce the new structure:

testWidgets('composer exposes shell and inner surface', (tester) async {
  await tester.pumpWidget(_buildTestApp(
    mode: MessageComposerMode.text,
    process: MessageComposerProcess.idle,
    hasMessage: false,
    isWaitingAgent: false,
  ));

  expect(find.byKey(messageComposerShellKey), findsOneWidget);
  expect(find.byKey(messageComposerInnerKey), findsOneWidget);
});

Add another test to ensure the hold-to-speak state stays within the same shell:

testWidgets('recording state keeps unified floating shell', (tester) async {
  await tester.pumpWidget(_buildTestApp(
    mode: MessageComposerMode.holdToSpeak,
    process: MessageComposerProcess.recording,
    hasMessage: false,
    isWaitingAgent: false,
  ));

  expect(find.byKey(messageComposerShellKey), findsOneWidget);
  expect(find.text('松开发送'), findsOneWidget);
});

Step 2: Run test to verify it fails

Run: flutter test apps/test/features/home/ui/widgets/home_composer_test.dart

Expected: FAIL because the new structure keys do not exist.

Step 3: Write minimal implementation

Refactor MessageComposer from a single decorated Container into a layered shell:

return Container(
  key: messageComposerShellKey,
  padding: const EdgeInsets.all(AppSpacing.sm),
  decoration: BoxDecoration(
    color: AppColors.homeComposerShell,
    borderRadius: BorderRadius.circular(AppRadius.xxl),
    border: Border.all(color: AppColors.homeComposerBorder),
    boxShadow: const [
      BoxShadow(...),
    ],
  ),
  child: Container(
    key: messageComposerInnerKey,
    decoration: BoxDecoration(
      color: AppColors.homeComposerInner,
      borderRadius: BorderRadius.circular(AppRadius.xl),
    ),
    child: Row(...),
  ),
);

Keep all current callbacks and AG-UI-adjacent behavior intact. Do not change event names or chat flow.

Step 4: Run test to verify it passes

Run: flutter test apps/test/features/home/ui/widgets/home_composer_test.dart

Expected: PASS, including the existing callback and state-priority assertions.

Step 5: Commit

git add apps/lib/shared/widgets/message_composer.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "feat: rebuild composer as floating input island"

Task 4: Integrate attachment strip into the input island stack

Files:

  • Create: apps/lib/features/home/ui/widgets/home_attachment_strip.dart
  • Modify: apps/lib/features/home/ui/screens/home_screen.dart

Step 1: Write the failing test

Add a widget test that verifies selected images render in a unified strip above the composer shell:

testWidgets('selected images render in attachment strip above composer', (tester) async {
  // Pump HomeScreen with seeded selected images via a test-only constructor hook.
  expect(find.byKey(homeAttachmentStripKey), findsOneWidget);
});

Step 2: Run test to verify it fails

Run: flutter test apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart

Expected: FAIL because the strip widget and key do not exist.

Step 3: Write minimal implementation

Create a dedicated strip widget that uses the same home surface language:

class HomeAttachmentStrip extends StatelessWidget {
  const HomeAttachmentStrip({
    super.key,
    required this.images,
    required this.onRemove,
  });

  final List<XFile> images;
  final ValueChanged<int> onRemove;

  @override
  Widget build(BuildContext context) {
    if (images.isEmpty) return const SizedBox.shrink();
    return Container(
      key: homeAttachmentStripKey,
      padding: const EdgeInsets.all(AppSpacing.sm),
      decoration: BoxDecoration(
        color: AppColors.homeAttachmentSurface,
        borderRadius: BorderRadius.circular(AppRadius.xl),
      ),
      child: Wrap(...),
    );
  }
}

Mount it in the same bottom stack as the composer, not in the main scroll column.

Step 4: Run test to verify it passes

Run: flutter test apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart

Expected: PASS.

Step 5: Commit

git add apps/lib/features/home/ui/widgets/home_attachment_strip.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart
git commit -m "feat: unify home attachments with composer stack"

Task 5: Recompose HomeScreen around stage + floating bottom stack

Files:

  • Modify: apps/lib/features/home/ui/screens/home_screen.dart
  • Reference: apps/lib/features/home/ui/screens/home_sheet.dart

Step 1: Write the failing test

Add a focused home screen layout test:

testWidgets('home screen shows floating header, conversation stage, and bottom input stack', (tester) async {
  await tester.pumpWidget(buildHomeScreenForTest());

  expect(find.byKey(homeFloatingHeaderKey), findsOneWidget);
  expect(find.byKey(homeConversationStageKey), findsOneWidget);
  expect(find.byKey(homeBottomInputStackKey), findsOneWidget);
});

Step 2: Run test to verify it fails

Run: flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart

Expected: FAIL because the layout keys are not present.

Step 3: Write minimal implementation

Refactor the page into a single Stack with explicit layers:

return Scaffold(
  body: SafeArea(
    child: Stack(
      children: [
        const Positioned.fill(child: HomeBackgroundField()),
        Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const HomeFloatingHeader(),
            Expanded(child: _buildConversationStage(context, state)),
          ],
        ),
        _buildBottomInputStack(context, state),
        if (_isRecording) _buildRecordingGestureOverlay(),
      ],
    ),
  ),
);

Inside _buildConversationStage, keep existing history/message rendering logic but place it inside a calmer stage container with stable bottom padding so the floating composer never overlaps message content.

Step 4: Run test to verify it passes

Run: flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart

Expected: PASS.

Step 5: Commit

git add apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart
git commit -m "feat: recompose home screen into layered assistant stage"

Task 6: Tune empty-state and waiting-state presentation without adding helper copy

Files:

  • Modify: apps/lib/features/home/ui/screens/home_screen.dart

Step 1: Write the failing test

Add a test that verifies the empty state uses a dedicated stage surface instead of only centered text:

testWidgets('empty state renders stage surface without relying on helper copy', (tester) async {
  await tester.pumpWidget(buildEmptyHomeScreenForTest());
  expect(find.byKey(homeConversationStageKey), findsOneWidget);
  expect(find.byKey(homeEmptyStateOrbKey), findsOneWidget);
});

Step 2: Run test to verify it fails

Run: flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart

Expected: FAIL because the empty-state focal surface does not exist.

Step 3: Write minimal implementation

Replace the current Center(Text('开始对话吧')) style empty state with a focal surface that uses shape, spacing, and a soft orb layer instead of explanatory copy:

Widget _buildEmptyConversationStage() {
  return Center(
    child: Container(
      key: homeEmptyStateOrbKey,
      width: 220,
      height: 220,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        gradient: RadialGradient(...),
      ),
    ),
  );
}

Waiting state should sit at the lower edge of the stage, visually connected to the composer instead of appearing as a detached loading row.

Step 4: Run test to verify it passes

Run: flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart

Expected: PASS.

Step 5: Commit

git add apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart
git commit -m "feat: refine home empty and waiting states"

Task 7: Verify visual refresh and document manual QA

Files:

  • Modify: docs/plans/2026-03-13-home-screen-visual-refresh.md

Step 1: Run automated verification

Run:

flutter test apps/test/features/home/ui/widgets/home_composer_test.dart apps/test/features/home/ui/widgets/home_background_field_test.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart

Expected: PASS.

Step 2: Run static verification

Run:

dart format apps/lib/core/theme/design_tokens.dart apps/lib/features/home/ui/screens/home_screen.dart apps/lib/features/home/ui/widgets/home_background_field.dart apps/lib/features/home/ui/widgets/home_floating_header.dart apps/lib/features/home/ui/widgets/home_attachment_strip.dart apps/lib/shared/widgets/message_composer.dart apps/test/features/home/ui/widgets/home_composer_test.dart apps/test/features/home/ui/widgets/home_background_field_test.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart
flutter analyze

Expected: PASS.

Step 3: Run manual QA

Verify on a phone-sized simulator or device:

1. 首页空态 first impression 不依赖提示文案,仍然有清晰主场感
2. 顶部浮层不抢焦点,底部输入岛是最稳定视觉锚点
3. 文本/语音/转写/等待态切换时,输入岛壳体保持连续
4. 附件预览与输入区属于同一层级,不再像临时插块
5. 消息列表滚动到底部时,不会被悬浮输入岛遮挡

Step 4: Update plan status note

Append a short verification note to this plan with pass/fail status and any follow-up token work.

Step 5: Commit

git add docs/plans/2026-03-13-home-screen-visual-refresh.md
git commit -m "docs: record home screen visual refresh verification"