diff --git a/.github/workflows/flutter_test.yml b/.github/workflows/flutter_test.yml index 750af000..1e975ef2 100644 --- a/.github/workflows/flutter_test.yml +++ b/.github/workflows/flutter_test.yml @@ -41,6 +41,14 @@ jobs: run: dart run tool/check_local_only_boundary.dart - name: Analyze run: flutter analyze + - name: Check Figma UI inventory and reference images + # Flutter goldens are generated review evidence and are intentionally + # not committed. CI still validates every route/state, tracked Figma + # reference hash, and the declared golden path. + run: >- + dart run tool/report_figma_ui_coverage.dart + --strict + --allow-missing-goldens - name: Run test with coverage run: flutter test --coverage - name: Check coverage diff --git a/assets/calendar_back.svg b/assets/calendar_back.svg new file mode 100644 index 00000000..f4a73928 --- /dev/null +++ b/assets/calendar_back.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/calendar_chevron_down.svg b/assets/calendar_chevron_down.svg new file mode 100644 index 00000000..627c372d --- /dev/null +++ b/assets/calendar_chevron_down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/calendar_edit.svg b/assets/calendar_edit.svg new file mode 100644 index 00000000..53487c9c --- /dev/null +++ b/assets/calendar_edit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/calendar_map_pin.svg b/assets/calendar_map_pin.svg new file mode 100644 index 00000000..c9d49ab6 --- /dev/null +++ b/assets/calendar_map_pin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/calendar_month_arrow.svg b/assets/calendar_month_arrow.svg new file mode 100644 index 00000000..4ac18280 --- /dev/null +++ b/assets/calendar_month_arrow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/calendar_trash.svg b/assets/calendar_trash.svg new file mode 100644 index 00000000..e1a9ae36 --- /dev/null +++ b/assets/calendar_trash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/design/destructive_warning.svg b/assets/design/destructive_warning.svg new file mode 100644 index 00000000..b775ded9 --- /dev/null +++ b/assets/design/destructive_warning.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/design/information_filled.svg b/assets/design/information_filled.svg new file mode 100644 index 00000000..b4c8805e --- /dev/null +++ b/assets/design/information_filled.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/design/information_outline.svg b/assets/design/information_outline.svg new file mode 100644 index 00000000..10a3ba2d --- /dev/null +++ b/assets/design/information_outline.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/design/notification_bell.svg b/assets/design/notification_bell.svg new file mode 100644 index 00000000..e89ad86d --- /dev/null +++ b/assets/design/notification_bell.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/design/onboarding_drag.svg b/assets/design/onboarding_drag.svg new file mode 100644 index 00000000..f5cd9ef5 --- /dev/null +++ b/assets/design/onboarding_drag.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/design/onboarding_greeting.png b/assets/design/onboarding_greeting.png new file mode 100644 index 00000000..e4342e1f Binary files /dev/null and b/assets/design/onboarding_greeting.png differ diff --git a/assets/design/onboarding_progress_complete.svg b/assets/design/onboarding_progress_complete.svg new file mode 100644 index 00000000..6fe0ea38 --- /dev/null +++ b/assets/design/onboarding_progress_complete.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/design/onboarding_progress_current.svg b/assets/design/onboarding_progress_current.svg new file mode 100644 index 00000000..10f46094 --- /dev/null +++ b/assets/design/onboarding_progress_current.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/design/onboarding_progress_last_current.svg b/assets/design/onboarding_progress_last_current.svg new file mode 100644 index 00000000..2fb4feab --- /dev/null +++ b/assets/design/onboarding_progress_last_current.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/design/onboarding_progress_last_pending.svg b/assets/design/onboarding_progress_last_pending.svg new file mode 100644 index 00000000..01255607 --- /dev/null +++ b/assets/design/onboarding_progress_last_pending.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/design/onboarding_progress_pending.svg b/assets/design/onboarding_progress_pending.svg new file mode 100644 index 00000000..e2879d3f --- /dev/null +++ b/assets/design/onboarding_progress_pending.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/design/recovery_spinner.svg b/assets/design/recovery_spinner.svg new file mode 100644 index 00000000..a6994995 --- /dev/null +++ b/assets/design/recovery_spinner.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/feedback_spinner.svg b/assets/feedback_spinner.svg new file mode 100644 index 00000000..a555f4a6 --- /dev/null +++ b/assets/feedback_spinner.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/home_mascot.png b/assets/home_mascot.png new file mode 100644 index 00000000..6ba0490e Binary files /dev/null and b/assets/home_mascot.png differ diff --git a/assets/home_nav_add.svg b/assets/home_nav_add.svg new file mode 100644 index 00000000..c01e92b3 --- /dev/null +++ b/assets/home_nav_add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/home_nav_home.svg b/assets/home_nav_home.svg new file mode 100644 index 00000000..7ed25060 --- /dev/null +++ b/assets/home_nav_home.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/home_nav_my.svg b/assets/home_nav_my.svg new file mode 100644 index 00000000..a273e406 --- /dev/null +++ b/assets/home_nav_my.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/my_page_bell.svg b/assets/my_page_bell.svg new file mode 100644 index 00000000..30994ec9 --- /dev/null +++ b/assets/my_page_bell.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/my_page_chevron.svg b/assets/my_page_chevron.svg new file mode 100644 index 00000000..b79a577d --- /dev/null +++ b/assets/my_page_chevron.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/my_page_clock.svg b/assets/my_page_clock.svg new file mode 100644 index 00000000..c67fce90 --- /dev/null +++ b/assets/my_page_clock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/my_page_download.svg b/assets/my_page_download.svg new file mode 100644 index 00000000..c018fc82 --- /dev/null +++ b/assets/my_page_download.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/my_page_person.svg b/assets/my_page_person.svg new file mode 100644 index 00000000..b5d406ab --- /dev/null +++ b/assets/my_page_person.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/my_page_phone.svg b/assets/my_page_phone.svg new file mode 100644 index 00000000..2eba581f --- /dev/null +++ b/assets/my_page_phone.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/my_page_repeat.svg b/assets/my_page_repeat.svg new file mode 100644 index 00000000..7d19cad2 --- /dev/null +++ b/assets/my_page_repeat.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/my_page_shield.svg b/assets/my_page_shield.svg new file mode 100644 index 00000000..6f511af3 --- /dev/null +++ b/assets/my_page_shield.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/my_page_stopwatch.svg b/assets/my_page_stopwatch.svg new file mode 100644 index 00000000..36d0285d --- /dev/null +++ b/assets/my_page_stopwatch.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/my_page_toggle_on.svg b/assets/my_page_toggle_on.svg new file mode 100644 index 00000000..f5d0699f --- /dev/null +++ b/assets/my_page_toggle_on.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/my_page_trash.svg b/assets/my_page_trash.svg new file mode 100644 index 00000000..b1528567 --- /dev/null +++ b/assets/my_page_trash.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/my_page_upload.svg b/assets/my_page_upload.svg new file mode 100644 index 00000000..08b4fbb0 --- /dev/null +++ b/assets/my_page_upload.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/preparation_edit_drag.svg b/assets/preparation_edit_drag.svg new file mode 100644 index 00000000..fe122130 --- /dev/null +++ b/assets/preparation_edit_drag.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/recurrence_date_time_calendar.svg b/assets/recurrence_date_time_calendar.svg new file mode 100644 index 00000000..e63d3d63 --- /dev/null +++ b/assets/recurrence_date_time_calendar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_date_time_chevron.svg b/assets/recurrence_date_time_chevron.svg new file mode 100644 index 00000000..769a8360 --- /dev/null +++ b/assets/recurrence_date_time_chevron.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_date_time_clock.svg b/assets/recurrence_date_time_clock.svg new file mode 100644 index 00000000..ebb50515 --- /dev/null +++ b/assets/recurrence_date_time_clock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/recurrence_date_time_repeat.svg b/assets/recurrence_date_time_repeat.svg new file mode 100644 index 00000000..a6058682 --- /dev/null +++ b/assets/recurrence_date_time_repeat.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_detached_timeicon.svg b/assets/recurrence_detached_timeicon.svg new file mode 100644 index 00000000..252ec95f --- /dev/null +++ b/assets/recurrence_detached_timeicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/recurrence_detached_vector.svg b/assets/recurrence_detached_vector.svg new file mode 100644 index 00000000..a8838ed8 --- /dev/null +++ b/assets/recurrence_detached_vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurrence_detail_frame.svg b/assets/recurrence_detail_frame.svg new file mode 100644 index 00000000..2007e861 --- /dev/null +++ b/assets/recurrence_detail_frame.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_detail_frame1.svg b/assets/recurrence_detail_frame1.svg new file mode 100644 index 00000000..adb738ed --- /dev/null +++ b/assets/recurrence_detail_frame1.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/recurrence_detail_frame2.svg b/assets/recurrence_detail_frame2.svg new file mode 100644 index 00000000..9b9d6cce --- /dev/null +++ b/assets/recurrence_detail_frame2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_detail_frame3.svg b/assets/recurrence_detail_frame3.svg new file mode 100644 index 00000000..6963a726 --- /dev/null +++ b/assets/recurrence_detail_frame3.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/recurrence_detail_frame4.svg b/assets/recurrence_detail_frame4.svg new file mode 100644 index 00000000..a503edd7 --- /dev/null +++ b/assets/recurrence_detail_frame4.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_end_confirm_vector.svg b/assets/recurrence_end_confirm_vector.svg new file mode 100644 index 00000000..5178b4e8 --- /dev/null +++ b/assets/recurrence_end_confirm_vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurrence_ending_ellipse.svg b/assets/recurrence_ending_ellipse.svg new file mode 100644 index 00000000..bdc834ae --- /dev/null +++ b/assets/recurrence_ending_ellipse.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurrence_occurrence_frame.svg b/assets/recurrence_occurrence_frame.svg new file mode 100644 index 00000000..2007e861 --- /dev/null +++ b/assets/recurrence_occurrence_frame.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_preparation_chevron.svg b/assets/recurrence_preparation_chevron.svg new file mode 100644 index 00000000..b79a577d --- /dev/null +++ b/assets/recurrence_preparation_chevron.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_preparation_drag.svg b/assets/recurrence_preparation_drag.svg new file mode 100644 index 00000000..d0028e00 --- /dev/null +++ b/assets/recurrence_preparation_drag.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/recurrence_preparation_underline.svg b/assets/recurrence_preparation_underline.svg new file mode 100644 index 00000000..8a7b8913 --- /dev/null +++ b/assets/recurrence_preparation_underline.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurrence_review_clock.svg b/assets/recurrence_review_clock.svg new file mode 100644 index 00000000..a74c781d --- /dev/null +++ b/assets/recurrence_review_clock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/recurrence_review_ellipse.svg b/assets/recurrence_review_ellipse.svg new file mode 100644 index 00000000..e3c420f9 --- /dev/null +++ b/assets/recurrence_review_ellipse.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurrence_review_play.svg b/assets/recurrence_review_play.svg new file mode 100644 index 00000000..edd7a09e --- /dev/null +++ b/assets/recurrence_review_play.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_save_error_vector1.svg b/assets/recurrence_save_error_vector1.svg new file mode 100644 index 00000000..67646c3b --- /dev/null +++ b/assets/recurrence_save_error_vector1.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurrence_scope_stateselected.svg b/assets/recurrence_scope_stateselected.svg new file mode 100644 index 00000000..4a00d64f --- /dev/null +++ b/assets/recurrence_scope_stateselected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/recurrence_scope_stateunselected.svg b/assets/recurrence_scope_stateunselected.svg new file mode 100644 index 00000000..4f306f5d --- /dev/null +++ b/assets/recurrence_scope_stateunselected.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurrence_scope_tonemuted.svg b/assets/recurrence_scope_tonemuted.svg new file mode 100644 index 00000000..0d4a1de8 --- /dev/null +++ b/assets/recurrence_scope_tonemuted.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/recurrence_scope_vector.svg b/assets/recurrence_scope_vector.svg new file mode 100644 index 00000000..ea97202b --- /dev/null +++ b/assets/recurrence_scope_vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurrence_time_exceptions_info.svg b/assets/recurrence_time_exceptions_info.svg new file mode 100644 index 00000000..40202690 --- /dev/null +++ b/assets/recurrence_time_exceptions_info.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/recurring_calendar_outline.svg b/assets/recurring_calendar_outline.svg new file mode 100644 index 00000000..97c38570 --- /dev/null +++ b/assets/recurring_calendar_outline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/recurring_card_calendar.svg b/assets/recurring_card_calendar.svg new file mode 100644 index 00000000..ca8d9dc2 --- /dev/null +++ b/assets/recurring_card_calendar.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/recurring_card_chevron.svg b/assets/recurring_card_chevron.svg new file mode 100644 index 00000000..2007e861 --- /dev/null +++ b/assets/recurring_card_chevron.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/recurring_card_clock.svg b/assets/recurring_card_clock.svg new file mode 100644 index 00000000..08201ce5 --- /dev/null +++ b/assets/recurring_card_clock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/runtime_step_check.svg b/assets/runtime_step_check.svg new file mode 100644 index 00000000..3882dd50 --- /dev/null +++ b/assets/runtime_step_check.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/design/figma_code_map.yaml b/docs/design/figma_code_map.yaml new file mode 100644 index 00000000..f84dd1fc --- /dev/null +++ b/docs/design/figma_code_map.yaml @@ -0,0 +1,768 @@ +# Reviewed means source and Flutter captures compared, with documented exceptions. +# Native verification and unmapped designs are reported separately. +--- +schema_version: 1 +figma_file_key: FIdR6bUMHScn9FWbwqrBsA +figma_page: '1:25' +figma_board: '285:5' +reference: + platform: ios + locale: ko + viewport: 390x844 + text_scale: 1.0 + flutter_commit: c7184b2a +screens: +- route: "/recovery" + figma_node: 1957:11762 + flutter: lib/presentation/startup/screens/local_data_recovery_screen.dart + state: default + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/recovery-v3.png + figma_sha256: '0458b76579f000c3ed4b40059836b04ed35e38797caddca79477b47a75c73ea4' + flutter_golden: test/goldens/goldens/recovery_default_390x844.png + variants: + - state: reset_confirmation + figma_node: 2003:12059 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/recovery-confirm-v3.png + figma_sha256: ce978101bf343b568497e4c381a9e461abfbc767a50cda7268af28a7231db784 + flutter_golden: test/goldens/goldens/recovery_reset_confirmation_390x844.png + - state: busy + figma_node: 2005:12036 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/recovery-busy.png + figma_sha256: 93b54f8e6116d4d793d98106eb180ba920109a8f09046804a0fe2bdc2e7083d3 + flutter_golden: test/goldens/goldens/recovery_busy_390x844.png +- route: "/startup" + figma_node: + flutter: lib/presentation/startup/screens/startup_screen.dart + status: excluded + reason: No corresponding current full-screen reference after the Screen Shells and + Permission Onboarding/Alarm Timer page audit. Native permission dialogs are platform-owned. + App route behavior remains tested. +- route: "/allowNotification" + figma_node: 246:230 + flutter: lib/presentation/notification_allow/screens/notification_allow_screen.dart + status: reviewed + figma_image: docs/design/review/figma/onboarding-10-20260925.png + figma_sha256: ceba9ea9bb5429e33ac8bd38332f90c93ca65878c60ae3d707e8a12a3e2e1b5e + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/notification_permission_390x844.png + state: request + variants: + - figma_node: 246:238 + status: excluded + figma_image: docs/design/review/figma/onboarding-11-20260925.png + figma_sha256: 12bd9bd4f0c706eb1ac9de5202595e20aa746ace1ee4bc84bf98743fa1565d2d + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + state: system_prompt + reason: OS-owned notification permission alert. Actual iOS request and denial + were exercised; app rationale is reviewed separately. +- route: "/allowAlarm" + figma_node: + flutter: lib/presentation/alarm_allow/screens/alarm_allow_screen.dart + status: excluded + reason: No corresponding current full-screen reference after the Screen Shells and + Permission Onboarding/Alarm Timer page audit. Native permission dialogs are platform-owned. + App route behavior remains tested. +- route: "/onboarding" + figma_node: '246:53' + flutter: lib/presentation/onboarding/screens/onboarding_screen.dart + status: reviewed + figma_image: docs/design/review/figma/onboarding-1-20260925.png + figma_sha256: a7cbab4c426baf609e2ce0b919a415cece70d0387199a1260e82ce1c8352c6d4 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_select_390x844.png + state: select + variants: + - figma_node: 246:90 + status: reviewed + figma_image: docs/design/review/figma/onboarding-2-20260925.png + figma_sha256: 51f0d329717347fd0df91b6b6756fa40112c2809da65511fb1fe688d4522d1c1 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_add_390x844.png + state: add + - figma_node: 246:113 + status: reviewed + figma_image: docs/design/review/figma/onboarding-3-20260925.png + figma_sha256: d89d8e494229a0e62ed43054028b7f1a46ac23017cc646dac83088456e18cdca + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_selected_390x844.png + state: selected + - figma_node: 246:150 + status: reviewed + figma_image: docs/design/review/figma/onboarding-4-20260925.png + figma_sha256: 79514dd77bc8bcf081cf8dfd3e813baf92e256c00715fdba94c85d2a26055f81 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_order_390x844.png + state: order + - figma_node: 246:180 + status: reviewed + figma_image: docs/design/review/figma/onboarding-5-20260925.png + figma_sha256: 681613261674d17d72260f67bc17f63574bebe11ac3bf9b8747d190b84fbc489 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_time_390x844.png + state: time + - figma_node: 246:212 + status: reviewed + figma_image: docs/design/review/figma/onboarding-6-20260925.png + figma_sha256: 0b148e6e8b357f3b41e298e508148176b183a7956b3acfc7b7135a242d81ed87 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_buffer_390x844.png + state: buffer + - figma_node: 278:100 + status: reviewed + figma_image: docs/design/review/figma/onboarding-7-20260925.png + figma_sha256: 389a73aa5b3fc1a3efec3077fd94695009a2f5ce5d00ec14469614a377bed3a8 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_time_focused_390x844.png + state: time_focused + - figma_node: 278:187 + status: reviewed + figma_image: docs/design/review/figma/onboarding-8-20260925.png + figma_sha256: 9a9ec8a31e93b79045623e66c7a01622bde52dcd0ddc89b15b2dd9ce3df7390b + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_time_short_390x844.png + state: time_short + - figma_node: 278:274 + status: reviewed + figma_image: docs/design/review/figma/onboarding-9-20260925.png + figma_sha256: bdceb130364cf9a585bc47426462460e408f24319290c131dfbce0b5637d9222 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_time_long_390x844.png + state: time_long +- route: "/onboarding/start" + figma_node: '246:42' + flutter: lib/presentation/onboarding/screens/onboarding_start_screen.dart + status: reviewed + figma_image: docs/design/review/figma/onboarding-0-20260925.png + figma_sha256: b4e365b2ebe04c028c77f6b47af4d71cd1eaf3601e67ef427652e6614f167a2d + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/onboarding-parity-20260925.md. + flutter_golden: test/goldens/goldens/onboarding_welcome_390x844.png + state: welcome +- route: "/home" + figma_node: 600:34301 + flutter: lib/presentation/home/screens/home_screen_tmp.dart + state: default + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar dates + and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/home-default.png + figma_sha256: 87d9c1e3ed1a5a556f2fbb8081abaacdbeb41fbbf19f4d1cf998e015fac70d40 + flutter_golden: test/goldens/goldens/home_default_390x844.png + variants: + - state: loading + figma_node: '1824:2' + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/home-loading.png + figma_sha256: 52362c7a344d8e4cf788b4ce9ac5bb89631e813a3a7f7e08ce9eb80cf041c6a7 + flutter_golden: test/goldens/goldens/home_loading_390x844.png + - state: error + figma_node: 1824:694 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/home-error.png + figma_sha256: 7aa8f80ce1ff05847209828f49045bc8e1d0a09803fd1625534860ff4d746281 + flutter_golden: test/goldens/goldens/home_error_390x844.png +- route: "/myPage" + figma_node: 2113:21761 + flutter: lib/presentation/my_page/my_page_screen.dart + state: default + status: reviewed + review_note: Current 390x844 recurring-entry source was compared with the Flutter + golden; section coordinates, Figma SVGs, data routes, selection switches, large + text, and accessibility checks pass. Native status bar and home indicator are + excluded. + figma_image: docs/design/review/figma/my-page-recurring-entry.png + figma_sha256: a591d7b496760b05155125578eacfdbcd753d024e3d73c5a1ebb4cc7dad42e4a + flutter_golden: test/goldens/goldens/my_page_default_390x844.png +- route: "/recurringSchedules" + figma_node: 2112:21676 + flutter: lib/presentation/recurring/recurring_management_screen.dart + state: management + status: reviewed + review_note: Matching two-active and one-ended 390x844 fixture was compared to Figma; + section positions, counts, 99px active cards, 71px ended card, and detail/end + behavior pass. Native chrome is excluded. + figma_image: docs/design/review/figma/recurring-management.png + figma_sha256: c0b41a0cc37879d73207caf9d369df7d05449d1997974179b12f5dadb56b1879 + flutter_golden: test/goldens/goldens/recurring_management_390x844.png + variants: + - state: detail + figma_node: 2112:21715 + status: reviewed + figma_image: docs/design/review/figma/recurring-detail-20260925.png + figma_sha256: 2d2dce2fd2043e9d091cab354aa346604a8d52db71eb131e075814a15d6de87e + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_detail_390x844.png + - state: empty + figma_node: 2112:21756 + status: reviewed + review_note: 390x844 info, empty, and action cards match Figma coordinates; large-text + and narrow-width layout tests pass. Native chrome is excluded. + figma_image: docs/design/review/figma/recurring-empty.png + figma_sha256: 168133393b3fbc76a76bf1ea2e36467ea4d92d098f94b1bfd8426cab1f64dfbc + flutter_golden: test/goldens/goldens/recurring_empty_390x844.png + - state: occurrence + figma_node: 2112:21768 + status: reviewed + figma_image: docs/design/review/figma/recurring-occurrence-20260925.png + figma_sha256: 31f7f534b0b8ccf5cde3c58b5027b85d40175bdce29d6939b0cfbd4a3c9c6d26 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_occurrence_390x844.png +- route: "/myData" + figma_node: 1948:12072 + flutter: lib/presentation/my_page/my_data_screen.dart + state: default + status: reviewed + review_note: App-owned content, 358x68 rows, and 18px row spacing checked against + node metadata. Native status bar and home indicator are excluded from Flutter + golden. + figma_image: docs/design/review/figma/my-data-default.png + figma_sha256: 5f2a48998edfd1b3b47d4bff0022f941626ea94fbe3cae36799260a23aa41ed7 + flutter_golden: test/goldens/goldens/my_data_default_390x844.png + variants: + - state: reset_confirmation + figma_node: 2003:12059 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/recovery-confirm-v3.png + figma_sha256: ce978101bf343b568497e4c381a9e461abfbc767a50cda7268af28a7231db784 + flutter_golden: test/goldens/goldens/my_data_reset_confirmation_390x844.png + - state: restore_preview_confirmation + figma_node: 1978:11791 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/my-data-restore-preview.png + figma_sha256: ffb62bd9764f8897e14d95f6f12334fd0be6f8ec0a30f31c615fd37ad68a5689 + flutter_golden: test/goldens/goldens/my_data_restore_preview_390x844.png + - state: backup_password_create + figma_node: 1979:11816 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/my-data-password-create.png + figma_sha256: 7e04e15b88d878162767d910c9d45f3703a2fbe6bf27d56522204cef66abb04b + flutter_golden: test/goldens/goldens/my_data_password_create_390x844.png + - state: backup_password_input + figma_node: 1980:11841 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/my-data-password-input.png + figma_sha256: 94dd76f87a6a7fe77bd9672bdb645f52aa91700e48b2eeab101fbf2f4507f0c1 + flutter_golden: test/goldens/goldens/my_data_password_input_390x844.png + - state: backup_password_invalid_length + figma_node: 1981:11866 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/my-data-password-invalid.png + figma_sha256: de016130f45b290c63baddad21479e1ed2b65de2701be604bd8432c55d67ec9f + flutter_golden: test/goldens/goldens/my_data_password_invalid_390x844.png + - state: backup_password_mismatch + figma_node: 1982:11891 + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/my-data-password-mismatch.png + figma_sha256: 894f3c951dfcb852e4f42b96c40cd19586434ac3eecda4aad3f8b5211f7f4621 + flutter_golden: test/goldens/goldens/my_data_password_mismatch_390x844.png + - state: freshness_checking + figma_node: 1985:11908 + status: reviewed + review_note: Pending asynchronous freshness lookup, 68px status card, and action + spacing compared. Native chrome is excluded. + figma_image: docs/design/review/figma/my-data-freshness-checking.png + figma_sha256: cd98356ebc2cfed2ec369acf4acbb1ae81d16bbd761ee1fba5228e0aad9689ea + flutter_golden: test/goldens/goldens/my_data_freshness_checking_390x844.png + - state: freshness_no_changes + figma_node: 1987:11925 + status: reviewed + review_note: No-changes text and status card compared. Native chrome is excluded. + figma_image: docs/design/review/figma/my-data-freshness-no-changes.png + figma_sha256: 42972adfb56946596e963d3bfa5311f6cdf5eace2fb6f914db203c4e8c1bea3d + flutter_golden: test/goldens/goldens/my_data_freshness_no_changes_390x844.png + - state: freshness_unexported_changes + figma_node: 1989:11942 + status: reviewed + review_note: Unexported-changes text and status card compared. Native chrome is + excluded. + figma_image: docs/design/review/figma/my-data-freshness-unexported.png + figma_sha256: bd2307f8dcf8fa3aa3b846d8bac6a4eb586f1fb5464fffb05c930d0a1631c91a + flutter_golden: test/goldens/goldens/my_data_freshness_unexported_390x844.png + - state: freshness_reminder_due + figma_node: 1991:11959 + status: reviewed + review_note: Two-line 30-day reminder grows the card without hiding actions; color + and 18px row spacing compared. Native chrome is excluded. + figma_image: docs/design/review/figma/my-data-freshness-reminder.png + figma_sha256: e36b489f777f60d72099d5a888b8f07108314eb344796c7b5ab2d335208f076c + flutter_golden: test/goldens/goldens/my_data_freshness_reminder_390x844.png + - state: busy + figma_node: 1996:11977 + status: reviewed + review_note: Disabled row opacity, exported 40px spinner at y466, and no concurrent + restore action compared. Native chrome is excluded. + figma_image: docs/design/review/figma/my-data-busy.png + figma_sha256: d70f19ee78f45d98b1e228997ac4e2baf68e1efd177821402bbd80fa2c544b33 + flutter_golden: test/goldens/goldens/my_data_busy_390x844.png + - state: export_success_snackbar + figma_node: 1999:11994 + status: reviewed + review_note: 358x44 result bar at y752 after successful export compared. Native + chrome is excluded. + figma_image: docs/design/review/figma/my-data-export-success.png + figma_sha256: edab25e7b2f4c5541cd01e1a2242e0aba01c4d1a341d481e86fddac5057a5aac + flutter_golden: test/goldens/goldens/my_data_export_success_390x844.png + - state: restore_success_snackbar + figma_node: 2000:12010 + status: reviewed + review_note: Result-bar visual compared through the shared UI helper; end-to-end + native restore remains pending. + figma_image: docs/design/review/figma/my-data-restore-success.png + figma_sha256: 86c4162179792464efd9da6cf615b2aa9f45100a342f83a59aa28a904b437d29 + flutter_golden: test/goldens/goldens/my_data_restore_success_390x844.png + - state: operation_failure_snackbar + figma_node: 2001:12027 + status: reviewed + review_note: Result-bar visual compared; failed export clears the busy state and + displays its error. Native chrome is excluded. + figma_image: docs/design/review/figma/my-data-operation-failure.png + figma_sha256: '0390e8213191266798c61e773409b6b0f3861027fbc1f94e28b1be6e0cc32e00' + flutter_golden: test/goldens/goldens/my_data_operation_failure_390x844.png +- route: "/privacyPolicy" + figma_node: 1950:11759 + flutter: lib/presentation/my_page/privacy_policy_screen.dart + state: default + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/privacy-v3.png + figma_sha256: 88f84355c58e153300ccd4cce29c235c6fa3d05309fde7ba1eed99c04a6e94a5 + flutter_golden: test/goldens/goldens/privacy_policy_390x844.png +- route: "/resetComplete" + figma_node: 1963:11762 + flutter: lib/presentation/my_page/my_data_screen.dart + state: default + status: reviewed + review_note: Compared with fresh source; lifecycle, accessible tap targets, bundled + font, native chrome, and source-copy exceptions are recorded in docs/design/review/recovery-parity-20260925.md. + figma_image: docs/design/review/figma/reset-complete-v3.png + figma_sha256: f98c8ab46909ad90ea9a602c8b768a7bd78fd819bb18f1bdc4f70a086230fd01 + flutter_golden: test/goldens/goldens/reset_complete_390x844.png +- route: "/defaultPreparationSpareTimeEdit" + figma_node: 600:35201 + flutter: lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart + state: six_step_fixture + status: reviewed + review_note: Korean 390x844 header, stepper, summary, 358x62 list rows, duration + inputs, and add action compared with Figma. Six linked steps are a reference fixture, + not a product maximum; a seventh can be added. Pretendard substitutes for Figma + Noto Sans KR and IBM Plex Sans KR; native chrome is excluded. + figma_image: docs/design/review/figma/preparation-spare-time-edit-default.png + figma_sha256: bcd3b05ab6da3335d054e26910d8fe60183a2ef65ae2b0908c89e1fd84672569 + flutter_golden: test/goldens/goldens/preparation_spare_time_edit_390x844.png +- route: "/calendar" + figma_node: 868:146599 + state: empty + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar dates + and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-empty.png + figma_sha256: 0a1bd7f2eb3afcc6c6809c8d9f7d05a904fcfcde6f13570188a97cc9bf242e8b + flutter_golden: test/goldens/goldens/calendar_empty_390x852.png + variants: + - state: expanded + figma_node: 868:146600 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-expanded.png + figma_sha256: 1583f6db767d15b1a7aea64dac8f51dba795d9c8d64339baeb42d6f747bae69d + flutter_golden: test/goldens/goldens/calendar_expanded_390x852.png + - state: list + figma_node: 882:426235 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-list.png + figma_sha256: 422d779410182dec6a1c5050d3b9c76c4a4de0116c16c946d0bfd400a091e3da + flutter_golden: test/goldens/goldens/calendar_list_390x852.png + - state: swipe_actions + figma_node: 882:464741 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-swipe-actions.png + figma_sha256: f9687add5e58cad41b146a3a448fe67e402a471d3b8d83277440be05e6983fee + flutter_golden: test/goldens/goldens/calendar_swipe_actions_390x852.png + - state: expanded_actions + figma_node: 882:493347 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-expanded-actions.png + figma_sha256: b475c56020d23f555bb07c44bf7dc3e201ffa170ef9473eccdf27ba370ec6aec + flutter_golden: test/goldens/goldens/calendar_expanded_actions_390x852.png + - state: delete_confirm + figma_node: 882:512625 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-delete-confirm.png + figma_sha256: a00d9ca5a8ad5a693f59da83cdfbcf72e8b34095fc9c101cc9aba9c4f77091c0 + flutter_golden: test/goldens/goldens/calendar_delete_confirm_390x852.png + - state: loading + figma_node: 1816:6728 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-loading.png + figma_sha256: d96b113898d510c8e11a391d6dc2f05a8671680a6986ef989464f297556a2be7 + flutter_golden: test/goldens/goldens/calendar_loading_390x852.png + - state: error + figma_node: 1816:6984 + status: reviewed + review_note: Compared with fresh source; keeps mathematically correct calendar + dates and actual model values. Exact geometry, interactions, font/native exceptions + and source defects are recorded in docs/design/review/calendar-parity-20260925.md. + figma_image: docs/design/review/figma/calendar-error.png + figma_sha256: a7d69eddcc876d99622541c561c0c81b55f24a3344af7bd72a505b34bd12f540 + flutter_golden: test/goldens/goldens/calendar_error_390x852.png +- route: "/scheduleCreate" + figma_node: 2112:2394 + state: date_time + status: reviewed + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + variants: + - state: weekly + figma_node: 2112:21323 + status: reviewed + figma_image: docs/design/review/figma/recurring-weekly-20260925.png + figma_sha256: 343688b5181b27e682b857de98fe8a907e6900521acaceb97f2e9c6e5891c82f + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_weekly_390x844.png + - state: monthly + figma_node: 2112:21352 + status: reviewed + figma_image: docs/design/review/figma/recurring-monthly-20260925.png + figma_sha256: b69b5931f22f8893e31338cfac15eb2c15b37be6c06b2864d787113e480e974b + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_monthly_390x844.png + - state: ending + figma_node: 2112:21395 + status: reviewed + figma_image: docs/design/review/figma/recurring-ending-20260925.png + figma_sha256: 22c1608681bd101b922d4deb4a1c1a0469cd15465a98d4d76d552a68be9b3688 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_ending_390x844.png + - state: preparation + figma_node: 2112:21456 + status: reviewed + figma_image: docs/design/review/figma/recurring-preparation-20260925.png + figma_sha256: ecb0df74ebe0e54067514f16be2d2a25a1dd1ccdec5bdc4b0073fd0107b59fe3 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_preparation_390x844.png + - state: review + figma_node: 2112:21500 + status: reviewed + figma_image: docs/design/review/figma/recurring-review-20260925.png + figma_sha256: 29a84cd7e21f2b9369967d8f26b9fcf1c25f20329ce584242b3db911cbe004fe + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_review_390x844.png + - state: conflicts + figma_node: 2112:21531 + status: reviewed + figma_image: docs/design/review/figma/recurring-conflicts-20260925.png + figma_sha256: c4d4be1b26f8f51628b55660a7eaa3f2cb9847d4b23b7f39658e9a52bb72f7fe + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_conflicts_390x844.png + - state: persistent_conflict + figma_node: 2112:21557 + status: reviewed + figma_image: docs/design/review/figma/recurring-persistent-conflict-20260925.png + figma_sha256: eed0e8720e4242851e2f518eb43ed98f44d0a991e15287aecd0ae6dd60474a00 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_persistent_conflict_390x844.png + - state: scope + figma_node: 2112:21588 + status: reviewed + figma_image: docs/design/review/figma/recurring-scope-20260925.png + figma_sha256: 6361ec89504c0d9deea00e69b22bdde1dd2ec6f7b10f816381e7ea8c55b245e5 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_scope_390x844.png + - state: detached + figma_node: 2112:21613 + status: reviewed + figma_image: docs/design/review/figma/recurring-detached-20260925.png + figma_sha256: d74e3d7294dbc3553fea3d445c7b1082a38a65eb1aab22f1a3d3cea136c1e5f1 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_detached_390x844.png + - state: time_exceptions + figma_node: 2112:21635 + status: reviewed + figma_image: docs/design/review/figma/recurring-time-exceptions-20260925.png + figma_sha256: 44c32c26792c5a4838587e221c1287eca2f3aaa1ecfd5f7b41462f198b13dabe + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_time_exceptions_390x844.png + - state: save_error + figma_node: 2112:21659 + status: reviewed + figma_image: docs/design/review/figma/recurring-save-error-20260925.png + figma_sha256: 19ceb72970e68136567e80473a5439485cab149f89d8f7d81b4043eafd1bd067 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_save_error_390x844.png + - state: frequency + figma_node: 2113:21627 + status: reviewed + figma_image: docs/design/review/figma/recurring-frequency-20260925.png + figma_sha256: b9e75a77c132f8b8e7995878160f1c329e655e40262617c9ef91b4096cac1357 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_frequency_390x844.png + - state: monthly_fields + figma_node: 2113:21670 + status: reviewed + figma_image: docs/design/review/figma/recurring-monthly-fields-20260925.png + figma_sha256: 657b3d1316485837fa641d87754d571060845e240c353a3ee9cf37ae4a2d405b + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_monthly_fields_390x844.png + - state: end_date + figma_node: 2113:21708 + status: reviewed + figma_image: docs/design/review/figma/recurring-end-date-20260925.png + figma_sha256: 5ec407e1334d1e0b012647fb172fbf30e249b606f8600c1c486c8ad857af9ae7 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_end_date_390x844.png + - state: end_confirm + figma_node: 2113:21745 + status: reviewed + figma_image: docs/design/review/figma/recurring-end-confirm-20260925.png + figma_sha256: 17fe56d97a952bcec836cd2fb6c9451eebdf10d2961819c1f729366979cec970 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/recurring_end_confirm_390x844.png + - figma_node: '251:42' + status: reviewed + figma_image: docs/design/review/figma/form-name.png + figma_sha256: 912f174f618dfd88e6bdec1ad0f1aca2ae403377e51fcbb9ddb234c8f6642965 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/original-form-parity-20260925.md. + flutter_golden: test/goldens/goldens/form_name_390x844.png + state: name + - figma_node: 251:75 + status: reviewed + figma_image: docs/design/review/figma/form-date.png + figma_sha256: 147cc7d396469cb4e11af53b1dca3c25c1d7da57cf75c2e5a315143eb6bdb616 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/original-form-parity-20260925.md. + flutter_golden: test/goldens/goldens/form_date_picker_390x844.png + state: date_picker + - figma_node: 251:119 + status: reviewed + figma_image: docs/design/review/figma/form-time.png + figma_sha256: 50aacc58ce78d7ed1c2e1ccb027260cf1cca8e7e17bd1e8e2f17feb638b64995 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/original-form-parity-20260925.md. + flutter_golden: test/goldens/goldens/form_time_picker_390x844.png + state: time_picker + - figma_node: 251:162 + status: reviewed + figma_image: docs/design/review/figma/form-place.png + figma_sha256: 2bb089c948342412305999ccc5303fecf43c751b3ccd0dcd7caa88acfd40996e + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/original-form-parity-20260925.md. + flutter_golden: test/goldens/goldens/form_place_390x844.png + state: place_travel + figma_image: docs/design/review/figma/recurring-date-time-20260925.png + figma_sha256: e81a92bc149ec372cf5440f1a15992a70af63509f97c01b6f009df2bb2c7bbdd + flutter_golden: test/goldens/goldens/recurring_date_time_390x844.png +- route: "/scheduleEdit/:scheduleId" + figma_node: '251:42' + status: reviewed + figma_image: docs/design/review/figma/form-name.png + figma_sha256: 912f174f618dfd88e6bdec1ad0f1aca2ae403377e51fcbb9ddb234c8f6642965 + review_note: Edit directly reuses the reviewed ScheduleMultiPageForm and KeyboardBackedBottomSheet; + this image covers that shared form body, not a separate edit-only Figma design. + Original values, following-edit scope and individual overrides are verified by + database integration tests. See original-form-parity-20260925.md and recurring-parity-20260925.md. + flutter_golden: test/goldens/goldens/form_name_390x844.png + state: shared_form + flutter: lib/presentation/schedule_create/screens/schedule_edit_screen.dart +- route: "/preparationEdit" + figma_node: 1334:2023 + status: reviewed + figma_image: docs/design/review/figma/form-preparation.png + figma_sha256: 0ea7a3def11759115f924c88cc93b9f4176c52a09a40876c4d2bd4ded1f30684 + review_note: Source and Flutter compared, with documented product, source-data, + fonts and native exceptions. See docs/design/review/original-form-parity-20260925.md. + flutter_golden: test/goldens/goldens/preparation_edit_390x844.png + state: default + flutter: lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/screens/preparation_edit_form.dart +- route: "/scheduleStart" + figma_node: + status: excluded + reason: App runtime route has no corresponding full-screen reference in the audited + current Screen Shells, Form Flows, Preparation, Alarm Timer and Permission Onboarding + pages. Behavior is covered by preparation flow widget tests; source absence is + not visual approval. +- route: "/alarmScreen" + figma_node: '118:2' + status: reviewed + flutter: lib/presentation/alarm/screens/alarm_screen.dart + state: on_time + figma_image: docs/design/review/figma/runtime-on-time.png + figma_sha256: c3505d61f25faefd8e715fdc37a42c41e079b65ad4ca25c4e7120d14844304ff + flutter_golden: test/goldens/goldens/runtime_on_time_390x844.png + review_note: Fresh source and live-state fixture compared. Keeps consistent model + values, real progress and finish-result navigation; accessibility, fonts, source-data + and native exceptions are documented in docs/design/review/runtime-parity-20260925.md. + variants: + - figma_node: '118:58' + state: late + status: reviewed + figma_image: docs/design/review/figma/runtime-late.png + figma_sha256: 62f98401c1dee4fff78c0e2ea38e8d6ed020ec3ca0df1723e6fdd075ab1d3b8d + flutter_golden: test/goldens/goldens/runtime_late_390x844.png + review_note: Fresh source and live-state fixture compared. Keeps consistent model + values, real progress and finish-result navigation; accessibility, fonts, source-data + and native exceptions are documented in docs/design/review/runtime-parity-20260925.md. + - figma_node: 118:114 + state: finish_confirm + status: reviewed + figma_image: docs/design/review/figma/runtime-finish-confirm.png + figma_sha256: d9eb91b1ea9a9e1e7a6cc124435f057ca4fd7cba5e07749d9f8733630680edcb + flutter_golden: test/goldens/goldens/runtime_finish_confirm_390x844.png + review_note: Fresh source and live-state fixture compared. Keeps consistent model + values, real progress and finish-result navigation; accessibility, fonts, source-data + and native exceptions are documented in docs/design/review/runtime-parity-20260925.md. +- route: "/earlyLate" + figma_node: + status: excluded + reason: App runtime route has no corresponding full-screen reference in the audited + current Screen Shells, Form Flows, Preparation, Alarm Timer and Permission Onboarding + pages. Behavior is covered by preparation flow widget tests; source absence is + not visual approval. +- route: "/moving" + figma_node: + status: excluded + reason: App runtime route has no corresponding full-screen reference in the audited + current Screen Shells, Form Flows, Preparation, Alarm Timer and Permission Onboarding + pages. Behavior is covered by preparation flow widget tests; source absence is + not visual approval. +hidden_historical_nodes: +- 466:6517 +- 600:36265 +- 600:36353 +source_pages: + '1:25': Screen Shells + '1:26': Form Flows + '1:21': Preparation + '1:23': Permission Onboarding +reviewed_at: '2026-09-25' +design_only_nodes: +- figma_node: 1756:8930 + state: ScheduleDetail/Upcoming + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1775:9192 + state: ScheduleDetail/PreparationDue + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1775:9197 + state: ScheduleDetail/Preparing + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1775:9202 + state: ScheduleDetail/CompletedOnTime + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1775:9207 + state: ScheduleDetail/CompletedLate + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1775:9212 + state: ScheduleDetail/CompletedUnknown + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1775:9217 + state: ScheduleDetail/PastUnrecorded + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1913:11757 + state: ScheduleDetail/Loading + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1913:12081 + state: ScheduleDetail/Refreshing + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1913:11857 + state: ScheduleDetail/NotFound + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. +- figma_node: 1913:11969 + state: ScheduleDetail/Failure + status: unmapped + reason: Standalone Schedule Detail design has no app route/implementation. Calendar + expandable cards are a different component. See docs/design/review/calendar-parity-20260925.md. diff --git a/docs/design/redesign-20260923/flutter/README.md b/docs/design/redesign-20260923/flutter/README.md index 978c8e62..640a8415 100644 --- a/docs/design/redesign-20260923/flutter/README.md +++ b/docs/design/redesign-20260923/flutter/README.md @@ -15,10 +15,10 @@ ## 검증과 시각 자료 - `flutter analyze --no-pub`: 오류 없음. -- 전체 테스트 **603개 통과**. 최종 명령과 결과는 [validation.txt](validation.txt)에 기록했다. +- 이 디렉터리의 [validation.txt](validation.txt)는 2026-09-23 첫 반영 시점의 기록이다. 메인 화면 갱신을 병합한 뒤 최신 테스트와 비교 범위는 [QA handoff](../../../../handoff/ios-ui-quality-figma-parity.md)와 [화면 매핑](../../figma_code_map.yaml)을 확인한다. - 390×844 Flutter 위젯 렌더 26개를 아래에 저장했다. 실제 iOS/Android 기기 캡처가 아니다. 테스트의 날짜·준비 단계·설정값은 fixture이며 제품 기본값을 바꾸지 않는다. - 320px 너비와 글자 1.6배에서 반복·복구 관련 상태를 검사하고, 긴 하단 액션은 필요 시 세로 배치한다. -- `comparison-*.png`: 왼쪽 Figma 원본, 오른쪽 Flutter 렌더. `contact-*.png`: Flutter 렌더 모음. +- `comparison-*.png`와 `contact-*.png`는 첫 반영 시점의 비교/모음 이미지다. 갱신된 반복 관리·빈 상태 단일 캡처는 `screenshots/management.png`, `screenshots/empty.png`이며, 최신 상태별 참조/골든은 [화면 매핑](../../figma_code_map.yaml)을 기준으로 한다. ## 적용 시 유지한 차이 diff --git a/docs/design/redesign-20260923/flutter/screenshots/empty.png b/docs/design/redesign-20260923/flutter/screenshots/empty.png index 5c3af73e..c98c9e0c 100644 Binary files a/docs/design/redesign-20260923/flutter/screenshots/empty.png and b/docs/design/redesign-20260923/flutter/screenshots/empty.png differ diff --git a/docs/design/redesign-20260923/flutter/screenshots/management.png b/docs/design/redesign-20260923/flutter/screenshots/management.png index a3c57e90..5e4da279 100644 Binary files a/docs/design/redesign-20260923/flutter/screenshots/management.png and b/docs/design/redesign-20260923/flutter/screenshots/management.png differ diff --git a/docs/design/review/calendar-parity-20260925.md b/docs/design/review/calendar-parity-20260925.md new file mode 100644 index 00000000..6e025731 --- /dev/null +++ b/docs/design/review/calendar-parity-20260925.md @@ -0,0 +1,48 @@ +# Home · Calendar Figma comparison — 2026-09-25 + +Canonical file: `FIdR6bUMHScn9FWbwqrBsA`. Fresh `get_design_context` and screenshots were read for Home Default/Loading/Error and Calendar Expanded/List/SwipeActions/ExpandedActions/DeleteConfirm. Calendar's shared month/card components were inspected through these full-fidelity contexts. Existing Empty/Loading/Error source PNGs are retained. + +## Compared states + +| State | Figma node | Source image | Flutter golden | Source SHA-256 | +|---|---|---|---|---| +| Home default | `600:34301` | `figma/home-default.png` | `home_default_390x844.png` | `87d9c1e3ed1a5a556f2fbb8081abaacdbeb41fbbf19f4d1cf998e015fac70d40` | +| Home loading | `1824:2` | `figma/home-loading.png` | `home_loading_390x844.png` | `52362c7a344d8e4cf788b4ce9ac5bb89631e813a3a7f7e08ce9eb80cf041c6a7` | +| Home error | `1824:694` | `figma/home-error.png` | `home_error_390x844.png` | `7aa8f80ce1ff05847209828f49045bc8e1d0a09803fd1625534860ff4d746281` | +| Calendar empty | `868:146599` | `figma/calendar-empty.png` | `calendar_empty_390x852.png` | `0a1bd7f2eb3afcc6c6809c8d9f7d05a904fcfcde6f13570188a97cc9bf242e8b` | +| Calendar expanded | `868:146600` | `figma/calendar-expanded.png` | `calendar_expanded_390x852.png` | `1583f6db767d15b1a7aea64dac8f51dba795d9c8d64339baeb42d6f747bae69d` | +| Calendar list | `882:426235` | `figma/calendar-list.png` | `calendar_list_390x852.png` | `422d779410182dec6a1c5050d3b9c76c4a4de0116c16c946d0bfd400a091e3da` | +| Calendar swipe actions | `882:464741` | `figma/calendar-swipe-actions.png` | `calendar_swipe_actions_390x852.png` | `f9687add5e58cad41b146a3a448fe67e402a471d3b8d83277440be05e6983fee` | +| Calendar expanded actions | `882:493347` | `figma/calendar-expanded-actions.png` | `calendar_expanded_actions_390x852.png` | `b475c56020d23f555bb07c44bf7dc3e201ffa170ef9473eccdf27ba370ec6aec` | +| Calendar delete confirmation | `882:512625` | `figma/calendar-delete-confirm.png` | `calendar_delete_confirm_390x852.png` | `a00d9ca5a8ad5a693f59da83cdfbcf72e8b34095fc9c101cc9aba9c4f77091c0` | +| Calendar loading | `1816:6728` | `figma/calendar-loading.png` | `calendar_loading_390x852.png` | `d96b113898d510c8e11a391d6dc2f05a8671680a6986ef989464f297556a2be7` | +| Calendar error | `1816:6984` | `figma/calendar-error.png` | `calendar_error_390x852.png` | `a7d69eddcc876d99622541c561c0c81b55f24a3344af7bd72a505b34bd12f540` | + +Source paths are relative to this directory. Goldens are under `test/goldens/goldens/`. + +## Changes and visual assessment + +- Home preserves the 230px hero, 360×137 today card at (15,177), 360×391 month card, and 76px add action. Bottom navigation matches the 81px source height and icon centers (80,310). Home's today-card shadow is painted above the following month surface and golden capture includes normal Flutter shadows. +- Compact Calendar month cells use 51px rows instead of 44px plus a large bottom gutter. Selected date uses a 40px circle and numeric day (no Korean `일` suffix). The source month card is (18,110,354,390); the selected-date heading/card stack is aligned with source y532/y592. +- Schedule cards use 82px collapsed and 192px expanded height, a 70px time column/divider, 21px content inset, and 16px row gaps. Dynamic appointment times use 12-hour notation because a separate AM/PM label is visible. +- Swipe actions use 82px source buttons, 8px collapsed/10px expanded gaps, exact source `#545454` edit / `#bf2e22` delete colors, and rounded viewport clipping. Source SVGs are local `assets/calendar_chevron_down.svg` (24×25), `calendar_map_pin.svg` (18×19), `calendar_edit.svg` (24×24), `calendar_trash.svg` (24×24). Header/month arrows and Home navigation also use local exact source SVGs (`calendar_back.svg`/`calendar_month_arrow.svg` 24×24; `home_nav_home.svg`/`home_nav_my.svg` 24×24; `home_nav_add.svg` 50×50). Each is nonempty and rendered at its root geometry; the map pin has an explicit 18×19 slot. +- Delete confirmation retains the revealed schedule/actions behind a 42% scrim, uses the existing 277px dialog with 16px padding and 12px gaps, and centers across the full viewport. Cancelling does not delete data. The configurable shared-dialog additions preserve default behavior for other call sites. +- Card content grows at enlarged text sizes rather than enforcing the source's fixed heights. Existing edit restrictions for past/preparing/early-started/completed schedules and recurring occurrence controls remain intact. + +## Explicit source exceptions + +These states can be recorded as reviewed with the following exceptions; they are not an assertion of pixel identity: + +1. **Calendar truth:** Figma labels the month December 2024 but its grid is November 2024 (day 1 on Friday; day 21 on Thursday). Actual December 2024 begins Sunday and day 21 is Saturday. Both Home and Calendar deliberately render real dates; weekday positions and marker positions therefore differ. This is a documented source inconsistency, not an unresolved request to hard-code an incorrect calendar. +2. **Fixture data:** Figma appointment cards contain placeholders (`시간`, `-시간--분`, `--분`). The deterministic Flutter fixture contains real 6:00/7:00 PM appointments, 80-minute travel, loaded 0-minute preparation, and 10-minute buffer. Data text is not replaced by source placeholder strings. +3. **Typography/system UI:** The app bundles Pretendard, while these source nodes use Noto Sans KR/IBM Plex Sans KR. Glyph metrics and small text offsets differ. Native status bar and home indicator are platform-owned and excluded. Source loading/error exports also clip the selected-date heading; Flutter retains the complete heading. + +## Unmapped full Schedule Detail design audit + +The newly discovered `ScreenShell/ScheduleDetail` nodes are a separate full-screen design, not `lib/presentation/calendar/component/schedule_detail.dart`. Fresh context/screenshot for Upcoming (`1756:8930`) shows a standalone header, event facts, four-stage timing timeline, preparation plan, and start action. Repository route/code search finds no matching schedule-detail route or full-screen implementation; the similarly named Flutter component is only a Calendar expandable card. + +Unmapped full-screen states: Upcoming `1756:8930`; PreparationDue `1775:9192`; Preparing `1775:9197`; CompletedOnTime `1775:9202`; CompletedLate `1775:9207`; CompletedUnknown `1775:9212`; PastUnrecorded `1775:9217`; Loading `1913:11757`; Refreshing `1913:12081`; NotFound `1913:11857`; Failure `1913:11969`. These must remain explicitly unmapped, not count as passed Calendar variants. Adding that product flow requires separate route/state implementation. + +## Verification + +Final source/Flutter screenshots were visually inspected after rendering. Focused Home/Calendar/shared-dialog/calendar-theme run: 105 tests, 103 passed initially; a standalone 2x test fixture was corrected to use the real scrollable card context and the marker-color assertion was updated to the Figma token. Both affected files were rerun: all 13 tests passed. The root full suite remains the final combined gate. Existing coverage includes real date selection/month arrows, loading/error retry, create refresh, delete failure, back navigation, edit/delete lifecycle guards, recurring controls, and compact/enlarged-text layout. Five new source-state goldens cover list/expanded/swipe/expanded-actions/delete confirmation; the delete capture asserts no deletion occurs before confirmation and cancellation leaves data intact. diff --git a/docs/design/review/completion-20260925.md b/docs/design/review/completion-20260925.md new file mode 100644 index 00000000..dbebefde --- /dev/null +++ b/docs/design/review/completion-20260925.md @@ -0,0 +1,87 @@ +# iOS UI quality and Figma parity completion — 2026-09-25 + +The implemented app's Figma mapping and visual review are complete with documented source/runtime adaptations. Strict coverage has no pending or in-progress app entry. This is distinct from physical-device acceptance and from implementing every design present in Figma. Delivery remains [Draft PR #596](https://github.com/DevKor-github/OnTime-front/pull/596). + +The implementation follows refreshed `main` `c7184b2a`, merged into the isolated QA branch at `64bc111d`. The canonical file is [OnTime Design System](https://www.figma.com/design/FIdR6bUMHScn9FWbwqrBsA/OnTime-Design-System), not the earlier `GiDCK...` link. The audit includes Screen Shells, Form Flows, Preparation, and Permission Onboarding; it is not limited to board `285:5`. + +## Coverage and completion meaning + +[The source-to-code manifest](../figma_code_map.yaml) passes strict validation with the following totals: + +| App inventory | Total | Reviewed | Excluded | Pending / in progress | +| --- | ---: | ---: | ---: | ---: | +| Route defaults | 21 | 16 | 5 | 0 | +| Variants | 58 | 57 | 1 | 0 | +| Combined app entries | 79 | **73** | 6 | **0** | + +The five excluded routes (`/startup`, `/allowAlarm`, `/scheduleStart`, `/earlyLate`, `/moving`) have no corresponding current full-screen reference after the source audit; their behavior remains tested. The excluded variant `246:238` is the native permission alert, whose app-owned rationale is reviewed separately. Excluded means a stated source/platform boundary, not visual approval. + +Separately, `design_only_nodes` contains **11 unmapped standalone Schedule Detail designs**: Upcoming, PreparationDue, Preparing, CompletedOnTime, CompletedLate, CompletedUnknown, PastUnrecorded, Loading, Refreshing, NotFound, and Failure. Repository audit found no matching full-screen route/implementation. They are not the expandable Calendar card and are not included among the 73 reviewed app entries. Their exact nodes are recorded in [the Schedule Detail audit](calendar-parity-20260925.md#unmapped-full-schedule-detail-design-audit); implementing that new flow is outside this completed parity pass. + +Reviewed entries connect a verified canonical source, an original export/hash, real Flutter components/fixtures, and a reviewed golden or documented component reuse. Goldens are regression evidence. They do not prove literal pixel equality, native alarm delivery, or physical-device performance. + +## Applied result + +Source assets, panel geometry, actions, selection controls, dialogs and preparation progress were aligned across the current app. The implementation retains the actual four-step appointment flow, date arithmetic, data-derived times/totals, explicit conflict exclusion, series-owned preparation, scope-aware edits, live timer state, and confirmation before destructive changes. Source inconsistencies and platform boundaries are explicitly documented instead of copied into product behavior. + +Independent final review also caught a timer/dialog race: automatic completion could open over a manual finish prompt. A shared dialog guard and current-schedule recheck prevent overlapping prompts, with two real-timer regressions covering completion before and after the user chooses to continue. + +The main adaptations are consistent across the reports: bundled Pretendard instead of the source's mixed fonts; native status bars/keyboards/pickers/permissions instead of drawn substitutes; accessible 44px targets and scrolling at large text; correct calendar dates and actual duration sums instead of inconsistent sample artwork; and restart guidance after closed-storage reset. `/scheduleEdit/:scheduleId` shares the creation components and has database-backed edit/override coverage; no separate edit-only source is claimed. + +## Evidence index + +| Evidence | Scope and interpretation | +| --- | --- | +| [Home and Calendar](calendar-parity-20260925.md) | Default/loading/error, calendar list/expanded/swipe/actions/delete; correct-date exception; 11 unmapped standalone detail designs. | +| [Original forms and preparation edit](original-form-parity-20260925.md) | Name/place inputs, date/time Cupertino pickers, preparation edit, actual totals, save/cancel and enlarged text. | +| [Recurring flows](recurring-parity-20260925.md) | 18 source states, creation/review/conflicts/scope/DST/failure/detail/end, source SHA-to-golden table, actual four-step and edit reuse boundaries. | +| [Onboarding and notification permission](onboarding-parity-20260925.md) | Eleven app-owned states, source artwork, selection/order/numeric entry/validation, focus and OS permission boundary. | +| [Recovery, local data and privacy](recovery-parity-20260925.md) | Recovery/busy/reset, password validation and restore preview, privacy, secure field behavior and closed-storage lifecycle. | +| [Preparation runtime](runtime-parity-20260925.md) | Live on-time/late preparation, restored ring progress, manual finish confirmation, actual state values and narrow/enlarged layouts. | +| [iOS native observations](native-20260925/README.md) | Real simulator setup and permission denial/defer; Files export/import; durable restore/reset/relaunch; device availability and execution limits. | + +These reports contain the focused test results and source/Flutter image evidence. Their results support the listed behaviors; the final combined regression result is recorded only below. + +## Native data lifecycle demonstrated + +The isolated **OnTime Parity Backup QA 20260925** simulator (iPhone17, iOS26.5; UUID `74CFB493-7B8B-4DFD-9DD6-F0E97E682FA7`) completed real numeric three-minute preparation input and saved a local profile with a 30-minute buffer. Actual notification and AlarmKit prompts were denied/deferred through the app's normal controls. + +The native Files export saved an **811-byte encrypted backup**. After the buffer was changed to **35 minutes**, selecting the same file decrypted it and produced a restore preview. Confirming restore reloaded the **30-minute buffer** and three-minute preparation. Reset completed, and a full process relaunch returned to Welcome. The external backup survived reset with unchanged SHA-256 `339fe8a981377bdc9ef538e3c08cbb970e73862aff1f2768d2a660eb5814989e`. Screenshots and the exact scope are linked in [the native record](native-20260925/README.md); neither backup bytes nor the QA passphrase are committed. + +That run exposed two actual integration defects and verified their repairs: iOS could not use the previous save-location API/type filter, and tapping the native duration row did not reliably focus the intended field. Export now uses the encrypted-file document picker bridge and import supplies a UTI; the row focuses/selects the actual numeric field. Native export cancellation is covered by automated tests, not claimed as an executed cancellation scenario. Successful alarm delivery and a fresh native appointment CRUD pass are not claimed by this data-lifecycle run. + +## Final combined validation + + +All commands below completed successfully on the final source state, including the two timer/dialog race regressions. + +| Validation | Result | +| --- | --- | +| `dart run build_runner build --delete-conflicting-outputs` | Exit 0; ignored generated artifacts regenerated, no generated Dart outputs committed. | +| `flutter analyze --no-pub` | Exit 0; no issues. | +| `flutter test --no-pub --coverage --concurrency=1 --reporter expanded` | Exit 0; **693 passed, 0 failed, 0 skipped**, 4m 29s. This was a comparison run without `--update-goldens`. | +| `dart run tool/check_coverage.dart --min=80` | Exit 0; **87.45% app-owned line coverage (11,969 / 13,686)**, above the 80% threshold. | +| `dart run tool/check_generated_dart_policy.dart` | Exit 0; generated outputs ignored and untracked. | +| `dart run tool/check_local_only_boundary.dart` | Exit 0; local-only boundary verified. | +| `dart run tool/report_figma_ui_coverage.dart --strict` | Exit 0; 73 app entries reviewed, zero pending/in progress, six exclusions; 11 design-only states explicitly separate. | +| `git diff --check` | Exit 0. | +| `flutter build ios --simulator --debug --no-pub` | Exit 0; Xcode build 76.9s, `build/ios/iphonesimulator/Runner.app`. | +| Final simulator install and launch | Exit 0; installed on the isolated QA simulator, launched successfully, and native accessibility read-back showed Welcome, explanatory text and Start. | +| Fresh upstream read | `git fetch origin main` succeeded; `git rev-list --count HEAD..origin/main` returned 0 before the delivery commit. | + +The coverage percentage uses the repository's app-owned LCOV filter, excluding generated/localization/schema files; it is not a claim that every source line or device behavior was executed. Remote CI is a separate receipt on Draft PR #596 and must be read against its current head commit. + +### Reproducible golden environment + +The first remote run on Ubuntu, [36079080226](https://github.com/DevKor-github/OnTime-front/actions/runs/36079080226), passed 632 tests and failed 61 golden comparisons. Every reported failure was a pixel mismatch; there were no non-golden failures. The reviewed images were generated on macOS 26.5.1 with Flutter 3.44.4. Cross-platform font rendering is not guaranteed to match, as described in [Flutter's comparator documentation](https://api.flutter.dev/flutter/flutter_test/GoldenFileComparator-class.html). + +The full CI job now runs on the supported [macOS 26 arm64 runner](https://docs.github.com/en/actions/reference/runners/github-hosted-runners), with Flutter 3.44.4 retained and Homebrew SQLite/libsodium dependencies. It still executes every test and the unchanged exact-pixel comparator, without skipping tests, increasing tolerances or updating goldens in CI. Failure PNGs are uploaded for review. Generate and compare these iOS-first goldens on this macOS/Flutter environment; an OS/SDK rendering change requires explicit image review. The current PR check is the authoritative remote result. + +The same quality workflow is reusable through `workflow_call`. Both Android internal and closed-testing deployment jobs require its successful completion before their existing Ubuntu build/upload jobs can run. Their duplicate cross-platform golden invocation is removed; the entire suite and coverage gate remain mandatory on the canonical renderer. Existing branch checks, staging environment, signing secrets and upload commands are unchanged. Neither Android deployment was dispatched as part of this QA task. + + +## Physical-device release gate + +`xcrun devicectl list devices` reported the paired iPhones/iPad unavailable. There is no physical-device success claim. Before device acceptance, verify font/layout on an attached iPhone, VoiceOver operation, background and locked-screen alarm delivery, and scrolling/frame-time performance. Simulator debugging and widget semantics checks cannot substitute for those observations. + +Draft PR #596 remains the review artifact. Source review, automated regression, the demonstrated native data flow, physical-device acceptance, PR review, merge and release are separate evidence boundaries. [The plan](../../../plans/ios-ui-quality-figma-parity.md) and [handoff](../../../handoff/ios-ui-quality-figma-parity.md) point to this report as the sole final combined-validation receipt. diff --git a/docs/design/review/figma/calendar-delete-confirm.png b/docs/design/review/figma/calendar-delete-confirm.png new file mode 100644 index 00000000..cbf85f84 Binary files /dev/null and b/docs/design/review/figma/calendar-delete-confirm.png differ diff --git a/docs/design/review/figma/calendar-empty.png b/docs/design/review/figma/calendar-empty.png new file mode 100644 index 00000000..e960a4d3 Binary files /dev/null and b/docs/design/review/figma/calendar-empty.png differ diff --git a/docs/design/review/figma/calendar-error.png b/docs/design/review/figma/calendar-error.png new file mode 100644 index 00000000..e3dc53e9 Binary files /dev/null and b/docs/design/review/figma/calendar-error.png differ diff --git a/docs/design/review/figma/calendar-expanded-actions.png b/docs/design/review/figma/calendar-expanded-actions.png new file mode 100644 index 00000000..e46e8358 Binary files /dev/null and b/docs/design/review/figma/calendar-expanded-actions.png differ diff --git a/docs/design/review/figma/calendar-expanded.png b/docs/design/review/figma/calendar-expanded.png new file mode 100644 index 00000000..0e5b2556 Binary files /dev/null and b/docs/design/review/figma/calendar-expanded.png differ diff --git a/docs/design/review/figma/calendar-list.png b/docs/design/review/figma/calendar-list.png new file mode 100644 index 00000000..eada73e8 Binary files /dev/null and b/docs/design/review/figma/calendar-list.png differ diff --git a/docs/design/review/figma/calendar-loading.png b/docs/design/review/figma/calendar-loading.png new file mode 100644 index 00000000..9ebbc8ef Binary files /dev/null and b/docs/design/review/figma/calendar-loading.png differ diff --git a/docs/design/review/figma/calendar-swipe-actions.png b/docs/design/review/figma/calendar-swipe-actions.png new file mode 100644 index 00000000..faa86f4b Binary files /dev/null and b/docs/design/review/figma/calendar-swipe-actions.png differ diff --git a/docs/design/review/figma/form-date.png b/docs/design/review/figma/form-date.png new file mode 100644 index 00000000..e8021c7f Binary files /dev/null and b/docs/design/review/figma/form-date.png differ diff --git a/docs/design/review/figma/form-name.png b/docs/design/review/figma/form-name.png new file mode 100644 index 00000000..8afc5dd9 Binary files /dev/null and b/docs/design/review/figma/form-name.png differ diff --git a/docs/design/review/figma/form-place.png b/docs/design/review/figma/form-place.png new file mode 100644 index 00000000..0da52bef Binary files /dev/null and b/docs/design/review/figma/form-place.png differ diff --git a/docs/design/review/figma/form-preparation.png b/docs/design/review/figma/form-preparation.png new file mode 100644 index 00000000..a50b62e1 Binary files /dev/null and b/docs/design/review/figma/form-preparation.png differ diff --git a/docs/design/review/figma/form-time.png b/docs/design/review/figma/form-time.png new file mode 100644 index 00000000..33cc0f6f Binary files /dev/null and b/docs/design/review/figma/form-time.png differ diff --git a/docs/design/review/figma/home-default.png b/docs/design/review/figma/home-default.png new file mode 100644 index 00000000..ad26e44b Binary files /dev/null and b/docs/design/review/figma/home-default.png differ diff --git a/docs/design/review/figma/home-error.png b/docs/design/review/figma/home-error.png new file mode 100644 index 00000000..57968aa5 Binary files /dev/null and b/docs/design/review/figma/home-error.png differ diff --git a/docs/design/review/figma/home-loading.png b/docs/design/review/figma/home-loading.png new file mode 100644 index 00000000..b09246f4 Binary files /dev/null and b/docs/design/review/figma/home-loading.png differ diff --git a/docs/design/review/figma/my-data-busy.png b/docs/design/review/figma/my-data-busy.png new file mode 100644 index 00000000..beb9696a Binary files /dev/null and b/docs/design/review/figma/my-data-busy.png differ diff --git a/docs/design/review/figma/my-data-default.png b/docs/design/review/figma/my-data-default.png new file mode 100644 index 00000000..6a6fe414 Binary files /dev/null and b/docs/design/review/figma/my-data-default.png differ diff --git a/docs/design/review/figma/my-data-export-success.png b/docs/design/review/figma/my-data-export-success.png new file mode 100644 index 00000000..9eaa18f5 Binary files /dev/null and b/docs/design/review/figma/my-data-export-success.png differ diff --git a/docs/design/review/figma/my-data-freshness-checking.png b/docs/design/review/figma/my-data-freshness-checking.png new file mode 100644 index 00000000..9dea446f Binary files /dev/null and b/docs/design/review/figma/my-data-freshness-checking.png differ diff --git a/docs/design/review/figma/my-data-freshness-no-changes.png b/docs/design/review/figma/my-data-freshness-no-changes.png new file mode 100644 index 00000000..8d9fc09f Binary files /dev/null and b/docs/design/review/figma/my-data-freshness-no-changes.png differ diff --git a/docs/design/review/figma/my-data-freshness-reminder.png b/docs/design/review/figma/my-data-freshness-reminder.png new file mode 100644 index 00000000..507f55b9 Binary files /dev/null and b/docs/design/review/figma/my-data-freshness-reminder.png differ diff --git a/docs/design/review/figma/my-data-freshness-unexported.png b/docs/design/review/figma/my-data-freshness-unexported.png new file mode 100644 index 00000000..2a712903 Binary files /dev/null and b/docs/design/review/figma/my-data-freshness-unexported.png differ diff --git a/docs/design/review/figma/my-data-operation-failure.png b/docs/design/review/figma/my-data-operation-failure.png new file mode 100644 index 00000000..1d0f74c6 Binary files /dev/null and b/docs/design/review/figma/my-data-operation-failure.png differ diff --git a/docs/design/review/figma/my-data-password-create.png b/docs/design/review/figma/my-data-password-create.png new file mode 100644 index 00000000..75b4feac Binary files /dev/null and b/docs/design/review/figma/my-data-password-create.png differ diff --git a/docs/design/review/figma/my-data-password-input.png b/docs/design/review/figma/my-data-password-input.png new file mode 100644 index 00000000..f0f1488c Binary files /dev/null and b/docs/design/review/figma/my-data-password-input.png differ diff --git a/docs/design/review/figma/my-data-password-invalid.png b/docs/design/review/figma/my-data-password-invalid.png new file mode 100644 index 00000000..594894cd Binary files /dev/null and b/docs/design/review/figma/my-data-password-invalid.png differ diff --git a/docs/design/review/figma/my-data-password-mismatch.png b/docs/design/review/figma/my-data-password-mismatch.png new file mode 100644 index 00000000..ca9814f0 Binary files /dev/null and b/docs/design/review/figma/my-data-password-mismatch.png differ diff --git a/docs/design/review/figma/my-data-reset-confirmation.png b/docs/design/review/figma/my-data-reset-confirmation.png new file mode 100644 index 00000000..0fbf9ce2 Binary files /dev/null and b/docs/design/review/figma/my-data-reset-confirmation.png differ diff --git a/docs/design/review/figma/my-data-restore-preview.png b/docs/design/review/figma/my-data-restore-preview.png new file mode 100644 index 00000000..f9ea0d8e Binary files /dev/null and b/docs/design/review/figma/my-data-restore-preview.png differ diff --git a/docs/design/review/figma/my-data-restore-success.png b/docs/design/review/figma/my-data-restore-success.png new file mode 100644 index 00000000..ef5cc5a3 Binary files /dev/null and b/docs/design/review/figma/my-data-restore-success.png differ diff --git a/docs/design/review/figma/my-page-default.png b/docs/design/review/figma/my-page-default.png new file mode 100644 index 00000000..ac2f6f06 Binary files /dev/null and b/docs/design/review/figma/my-page-default.png differ diff --git a/docs/design/review/figma/my-page-recurring-entry.png b/docs/design/review/figma/my-page-recurring-entry.png new file mode 100644 index 00000000..f3c2eff4 Binary files /dev/null and b/docs/design/review/figma/my-page-recurring-entry.png differ diff --git a/docs/design/review/figma/onboarding-0-20260925.png b/docs/design/review/figma/onboarding-0-20260925.png new file mode 100644 index 00000000..3089ea74 Binary files /dev/null and b/docs/design/review/figma/onboarding-0-20260925.png differ diff --git a/docs/design/review/figma/onboarding-1-20260925.png b/docs/design/review/figma/onboarding-1-20260925.png new file mode 100644 index 00000000..45e85e3d Binary files /dev/null and b/docs/design/review/figma/onboarding-1-20260925.png differ diff --git a/docs/design/review/figma/onboarding-10-20260925.png b/docs/design/review/figma/onboarding-10-20260925.png new file mode 100644 index 00000000..7ce755ce Binary files /dev/null and b/docs/design/review/figma/onboarding-10-20260925.png differ diff --git a/docs/design/review/figma/onboarding-11-20260925.png b/docs/design/review/figma/onboarding-11-20260925.png new file mode 100644 index 00000000..e0a90836 Binary files /dev/null and b/docs/design/review/figma/onboarding-11-20260925.png differ diff --git a/docs/design/review/figma/onboarding-2-20260925.png b/docs/design/review/figma/onboarding-2-20260925.png new file mode 100644 index 00000000..47a29b4d Binary files /dev/null and b/docs/design/review/figma/onboarding-2-20260925.png differ diff --git a/docs/design/review/figma/onboarding-3-20260925.png b/docs/design/review/figma/onboarding-3-20260925.png new file mode 100644 index 00000000..c82fd22f Binary files /dev/null and b/docs/design/review/figma/onboarding-3-20260925.png differ diff --git a/docs/design/review/figma/onboarding-4-20260925.png b/docs/design/review/figma/onboarding-4-20260925.png new file mode 100644 index 00000000..c20fd768 Binary files /dev/null and b/docs/design/review/figma/onboarding-4-20260925.png differ diff --git a/docs/design/review/figma/onboarding-5-20260925.png b/docs/design/review/figma/onboarding-5-20260925.png new file mode 100644 index 00000000..3a179607 Binary files /dev/null and b/docs/design/review/figma/onboarding-5-20260925.png differ diff --git a/docs/design/review/figma/onboarding-6-20260925.png b/docs/design/review/figma/onboarding-6-20260925.png new file mode 100644 index 00000000..a4180fc5 Binary files /dev/null and b/docs/design/review/figma/onboarding-6-20260925.png differ diff --git a/docs/design/review/figma/onboarding-7-20260925.png b/docs/design/review/figma/onboarding-7-20260925.png new file mode 100644 index 00000000..a27022ad Binary files /dev/null and b/docs/design/review/figma/onboarding-7-20260925.png differ diff --git a/docs/design/review/figma/onboarding-8-20260925.png b/docs/design/review/figma/onboarding-8-20260925.png new file mode 100644 index 00000000..1f0001d3 Binary files /dev/null and b/docs/design/review/figma/onboarding-8-20260925.png differ diff --git a/docs/design/review/figma/onboarding-9-20260925.png b/docs/design/review/figma/onboarding-9-20260925.png new file mode 100644 index 00000000..f3fb02a3 Binary files /dev/null and b/docs/design/review/figma/onboarding-9-20260925.png differ diff --git a/docs/design/review/figma/preparation-spare-time-edit-default.png b/docs/design/review/figma/preparation-spare-time-edit-default.png new file mode 100644 index 00000000..e7ab89d5 Binary files /dev/null and b/docs/design/review/figma/preparation-spare-time-edit-default.png differ diff --git a/docs/design/review/figma/privacy-policy-default.png b/docs/design/review/figma/privacy-policy-default.png new file mode 100644 index 00000000..5a8dfde0 Binary files /dev/null and b/docs/design/review/figma/privacy-policy-default.png differ diff --git a/docs/design/review/figma/privacy-v3.png b/docs/design/review/figma/privacy-v3.png new file mode 100644 index 00000000..5c0b97a3 Binary files /dev/null and b/docs/design/review/figma/privacy-v3.png differ diff --git a/docs/design/review/figma/recovery-busy.png b/docs/design/review/figma/recovery-busy.png new file mode 100644 index 00000000..286994fd Binary files /dev/null and b/docs/design/review/figma/recovery-busy.png differ diff --git a/docs/design/review/figma/recovery-confirm-v3.png b/docs/design/review/figma/recovery-confirm-v3.png new file mode 100644 index 00000000..0e7379ff Binary files /dev/null and b/docs/design/review/figma/recovery-confirm-v3.png differ diff --git a/docs/design/review/figma/recovery-default.png b/docs/design/review/figma/recovery-default.png new file mode 100644 index 00000000..0099a063 Binary files /dev/null and b/docs/design/review/figma/recovery-default.png differ diff --git a/docs/design/review/figma/recovery-reset-confirmation.png b/docs/design/review/figma/recovery-reset-confirmation.png new file mode 100644 index 00000000..d5fb5a26 Binary files /dev/null and b/docs/design/review/figma/recovery-reset-confirmation.png differ diff --git a/docs/design/review/figma/recovery-v3.png b/docs/design/review/figma/recovery-v3.png new file mode 100644 index 00000000..97cb878c Binary files /dev/null and b/docs/design/review/figma/recovery-v3.png differ diff --git a/docs/design/review/figma/recurring-conflicts-20260925.png b/docs/design/review/figma/recurring-conflicts-20260925.png new file mode 100644 index 00000000..6bb1cd14 Binary files /dev/null and b/docs/design/review/figma/recurring-conflicts-20260925.png differ diff --git a/docs/design/review/figma/recurring-date-time-20260925.png b/docs/design/review/figma/recurring-date-time-20260925.png new file mode 100644 index 00000000..49901955 Binary files /dev/null and b/docs/design/review/figma/recurring-date-time-20260925.png differ diff --git a/docs/design/review/figma/recurring-detached-20260925.png b/docs/design/review/figma/recurring-detached-20260925.png new file mode 100644 index 00000000..c0ee1f0d Binary files /dev/null and b/docs/design/review/figma/recurring-detached-20260925.png differ diff --git a/docs/design/review/figma/recurring-detail-20260925.png b/docs/design/review/figma/recurring-detail-20260925.png new file mode 100644 index 00000000..6f65a884 Binary files /dev/null and b/docs/design/review/figma/recurring-detail-20260925.png differ diff --git a/docs/design/review/figma/recurring-empty.png b/docs/design/review/figma/recurring-empty.png new file mode 100644 index 00000000..4d7db68a Binary files /dev/null and b/docs/design/review/figma/recurring-empty.png differ diff --git a/docs/design/review/figma/recurring-end-confirm-20260925.png b/docs/design/review/figma/recurring-end-confirm-20260925.png new file mode 100644 index 00000000..5c649db8 Binary files /dev/null and b/docs/design/review/figma/recurring-end-confirm-20260925.png differ diff --git a/docs/design/review/figma/recurring-end-date-20260925.png b/docs/design/review/figma/recurring-end-date-20260925.png new file mode 100644 index 00000000..5af94e98 Binary files /dev/null and b/docs/design/review/figma/recurring-end-date-20260925.png differ diff --git a/docs/design/review/figma/recurring-ending-20260925.png b/docs/design/review/figma/recurring-ending-20260925.png new file mode 100644 index 00000000..9638ccb3 Binary files /dev/null and b/docs/design/review/figma/recurring-ending-20260925.png differ diff --git a/docs/design/review/figma/recurring-frequency-20260925.png b/docs/design/review/figma/recurring-frequency-20260925.png new file mode 100644 index 00000000..75116d73 Binary files /dev/null and b/docs/design/review/figma/recurring-frequency-20260925.png differ diff --git a/docs/design/review/figma/recurring-management.png b/docs/design/review/figma/recurring-management.png new file mode 100644 index 00000000..8f0483db Binary files /dev/null and b/docs/design/review/figma/recurring-management.png differ diff --git a/docs/design/review/figma/recurring-monthly-20260925.png b/docs/design/review/figma/recurring-monthly-20260925.png new file mode 100644 index 00000000..cdabe60b Binary files /dev/null and b/docs/design/review/figma/recurring-monthly-20260925.png differ diff --git a/docs/design/review/figma/recurring-monthly-fields-20260925.png b/docs/design/review/figma/recurring-monthly-fields-20260925.png new file mode 100644 index 00000000..6776ed3b Binary files /dev/null and b/docs/design/review/figma/recurring-monthly-fields-20260925.png differ diff --git a/docs/design/review/figma/recurring-occurrence-20260925.png b/docs/design/review/figma/recurring-occurrence-20260925.png new file mode 100644 index 00000000..32a59845 Binary files /dev/null and b/docs/design/review/figma/recurring-occurrence-20260925.png differ diff --git a/docs/design/review/figma/recurring-persistent-conflict-20260925.png b/docs/design/review/figma/recurring-persistent-conflict-20260925.png new file mode 100644 index 00000000..ec54ad5e Binary files /dev/null and b/docs/design/review/figma/recurring-persistent-conflict-20260925.png differ diff --git a/docs/design/review/figma/recurring-preparation-20260925.png b/docs/design/review/figma/recurring-preparation-20260925.png new file mode 100644 index 00000000..605dd02a Binary files /dev/null and b/docs/design/review/figma/recurring-preparation-20260925.png differ diff --git a/docs/design/review/figma/recurring-review-20260925.png b/docs/design/review/figma/recurring-review-20260925.png new file mode 100644 index 00000000..96119899 Binary files /dev/null and b/docs/design/review/figma/recurring-review-20260925.png differ diff --git a/docs/design/review/figma/recurring-save-error-20260925.png b/docs/design/review/figma/recurring-save-error-20260925.png new file mode 100644 index 00000000..740688f8 Binary files /dev/null and b/docs/design/review/figma/recurring-save-error-20260925.png differ diff --git a/docs/design/review/figma/recurring-scope-20260925.png b/docs/design/review/figma/recurring-scope-20260925.png new file mode 100644 index 00000000..d8338429 Binary files /dev/null and b/docs/design/review/figma/recurring-scope-20260925.png differ diff --git a/docs/design/review/figma/recurring-time-exceptions-20260925.png b/docs/design/review/figma/recurring-time-exceptions-20260925.png new file mode 100644 index 00000000..8673737e Binary files /dev/null and b/docs/design/review/figma/recurring-time-exceptions-20260925.png differ diff --git a/docs/design/review/figma/recurring-weekly-20260925.png b/docs/design/review/figma/recurring-weekly-20260925.png new file mode 100644 index 00000000..e3c9ef77 Binary files /dev/null and b/docs/design/review/figma/recurring-weekly-20260925.png differ diff --git a/docs/design/review/figma/reset-complete-v3.png b/docs/design/review/figma/reset-complete-v3.png new file mode 100644 index 00000000..c1f75131 Binary files /dev/null and b/docs/design/review/figma/reset-complete-v3.png differ diff --git a/docs/design/review/figma/reset-complete.png b/docs/design/review/figma/reset-complete.png new file mode 100644 index 00000000..e4acbc43 Binary files /dev/null and b/docs/design/review/figma/reset-complete.png differ diff --git a/docs/design/review/figma/runtime-finish-confirm.png b/docs/design/review/figma/runtime-finish-confirm.png new file mode 100644 index 00000000..20529185 Binary files /dev/null and b/docs/design/review/figma/runtime-finish-confirm.png differ diff --git a/docs/design/review/figma/runtime-late.png b/docs/design/review/figma/runtime-late.png new file mode 100644 index 00000000..9c36723f Binary files /dev/null and b/docs/design/review/figma/runtime-late.png differ diff --git a/docs/design/review/figma/runtime-on-time.png b/docs/design/review/figma/runtime-on-time.png new file mode 100644 index 00000000..e268a929 Binary files /dev/null and b/docs/design/review/figma/runtime-on-time.png differ diff --git a/docs/design/review/figma/today-status-scheduled.png b/docs/design/review/figma/today-status-scheduled.png new file mode 100644 index 00000000..5e7b258b Binary files /dev/null and b/docs/design/review/figma/today-status-scheduled.png differ diff --git a/docs/design/review/native-20260925/README.md b/docs/design/review/native-20260925/README.md new file mode 100644 index 00000000..52508e6c --- /dev/null +++ b/docs/design/review/native-20260925/README.md @@ -0,0 +1,33 @@ +# iOS native verification — 2026-09-25 + +The isolated **OnTime Parity Backup QA 20260925** simulator is iPhone 17 / iOS 26.5, UUID `74CFB493-7B8B-4DFD-9DD6-F0E97E682FA7`, bundle `club.devkor.ontime.ios`, debug build of this worktree. No existing device profile was reset. Its new local profile had one 3-minute preparation step, 30-minute buffer, and no appointments. System language was English; existing My Data screens and user-editable seed names remain Korean. + +## Executed, observed results + +| Scenario | Observation | +| --- | --- | +| New installation | Welcome → selection → order → numeric 3-minute entry → buffer → local profile saved. Native testing found that the time field's accessibility bounds merged with its row; the row now focuses/selects the real field and the native input succeeded after rebuilding. | +| Notifications | App rationale opened the actual iOS notification prompt. Denying permission continued to the alarm gate. | +| AlarmKit | The actual iOS alarm/timer permission prompt appeared. Denial retained the rationale; defer continued to Home. My Page showed notifications Off. No permission bypass or settings override was used. | +| Export | Entered a QA-only passphrase; the actual iOS Files document export picker opened. Saved `OnTime-20260925.ontimebackup` under On My iPhone. My Data changed to no changes since backup. See [saved state](backup-saved.png). | +| Durable setting change | Changed the default buffer 30→35 minutes and saved. My Page read 35 minutes. See [before restore](buffer-changed-35.png). | +| Import and validation | The native file-open picker showed the same 811-byte file; selecting it successfully decrypted and produced a preview for iOS, 1.1.0+56, zero appointments and one default preparation. See [preview](restore-preview.png). | +| Restore application | Confirmed restore; the app reported success. My Page reloaded the persisted 3-minute preparation and **30-minute** buffer. See [restored values](restored-buffer-30.png). | +| Reset lifecycle | Reset confirmation → busy → complete succeeded. See [completion](reset-complete.png). After full process termination/relaunch, Welcome appeared for a new local profile. See [relaunch](reset-relaunch-welcome.png). | +| External backup preservation | After reset/relaunch, the exported file still existed and its SHA-256 was unchanged. | + +File verification: 811 bytes, SHA-256 `339fe8a981377bdc9ef538e3c08cbb970e73862aff1f2768d2a660eb5814989e`. The encrypted bytes did not contain the sample preparation name in plaintext. Successful decryption/preview and the restored buffer value are the substantive restore evidence; filename/size alone would not be sufficient. No backup file or passphrase is committed. + +## Native defect repaired + +Installed `file_selector_ios` 0.5.3+6 implements open-file selection but does not implement the save-location API previously used by BackupService. Its type filtering also requires `uniformTypeIdentifiers`, which the old backup group omitted. Thus widget tests with a fake service could not establish that the production iOS operation worked. + +`BackupFilePicker` now routes iOS export through `on_time_front/backup_files`. Swift writes only the already-encrypted container to a temporary directory and presents [`UIDocumentPickerViewController(forExporting:asCopy:)`](https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller/init%28forexporting%3Aascopy%3A%29). The delegate reports success after the user saves, false on cancellation/dismissal, rejects concurrent export, and cleans its temporary files. The password stays in Dart. Non-iOS save-location handling is retained. Import uses the iOS `public.data` type; authenticated format validation determines whether the chosen file is a valid backup. + +Tests separately verify encrypted bytes/name crossing the channel, cancellation, native errors, unchanged freshness on failed/cancelled export, completion-only snapshot marking, preview without applying, invalid files and unchanged data, replacement restore, legacy format compatibility and recurring relationships. A second agent reviewed the native diff and found no blocking issue. Unit tests do not substitute for the Files UI observations above. + +## Evidence limits + +- This run exercised native setup, settings persistence, permission denial/defer, export, import, restore, reset and relaunch. It did not claim successful alarm delivery or a native export-cancel run; those gateway/cancellation paths have automated tests. +- Appointment creation/edit/delete and recurring scope/conflict/ownership are covered by the current widget/database integration suite. Earlier native recurring creation/navigation evidence is dated separately in `docs/design/recurring-schedules/implementation/native-ios/README.md`; it is not re-labelled as this run. +- No physical device was available: `xcrun devicectl list devices` returned the paired iPhones/iPad as unavailable. Physical-device font/layout, background/locked-screen alarm delivery, VoiceOver operation and frame-time/scroll performance remain a device acceptance gate. Debug simulator rendering is not a performance benchmark. diff --git a/docs/design/review/native-20260925/alarm-denied-rationale.png b/docs/design/review/native-20260925/alarm-denied-rationale.png new file mode 100644 index 00000000..a3b9c957 Binary files /dev/null and b/docs/design/review/native-20260925/alarm-denied-rationale.png differ diff --git a/docs/design/review/native-20260925/backup-saved.png b/docs/design/review/native-20260925/backup-saved.png new file mode 100644 index 00000000..d57dde0b Binary files /dev/null and b/docs/design/review/native-20260925/backup-saved.png differ diff --git a/docs/design/review/native-20260925/buffer-changed-35.png b/docs/design/review/native-20260925/buffer-changed-35.png new file mode 100644 index 00000000..3b9b6423 Binary files /dev/null and b/docs/design/review/native-20260925/buffer-changed-35.png differ diff --git a/docs/design/review/native-20260925/reset-complete.png b/docs/design/review/native-20260925/reset-complete.png new file mode 100644 index 00000000..25baf3f3 Binary files /dev/null and b/docs/design/review/native-20260925/reset-complete.png differ diff --git a/docs/design/review/native-20260925/reset-relaunch-welcome.png b/docs/design/review/native-20260925/reset-relaunch-welcome.png new file mode 100644 index 00000000..d7c3543c Binary files /dev/null and b/docs/design/review/native-20260925/reset-relaunch-welcome.png differ diff --git a/docs/design/review/native-20260925/restore-preview.png b/docs/design/review/native-20260925/restore-preview.png new file mode 100644 index 00000000..bbc26c72 Binary files /dev/null and b/docs/design/review/native-20260925/restore-preview.png differ diff --git a/docs/design/review/native-20260925/restored-buffer-30.png b/docs/design/review/native-20260925/restored-buffer-30.png new file mode 100644 index 00000000..a4ac6f10 Binary files /dev/null and b/docs/design/review/native-20260925/restored-buffer-30.png differ diff --git a/docs/design/review/onboarding-parity-20260925.md b/docs/design/review/onboarding-parity-20260925.md new file mode 100644 index 00000000..562c470d --- /dev/null +++ b/docs/design/review/onboarding-parity-20260925.md @@ -0,0 +1,59 @@ +# Onboarding and notification permission review — 2026-09-25 + +The canonical source is Figma `FIdR6bUMHScn9FWbwqrBsA`, page `1:23`, Onboarding/FlowScreens and Permission/NotificationPermission families. These active app surfaces must not be excluded merely because they are absent from board `285:5`. Every node below was read with fresh design context and screenshot. + +## Sources and captures + +| Node | State | Source image | SHA-256 | Flutter golden | +| --- | --- | --- | --- | --- | +| `246:42` | welcome | `docs/design/review/figma/onboarding-0-20260925.png` | `b4e365b2ebe04c028c77f6b47af4d71cd1eaf3601e67ef427652e6614f167a2d` | `test/goldens/goldens/onboarding_welcome_390x844.png` | +| `246:53` | select | `docs/design/review/figma/onboarding-1-20260925.png` | `a7cbab4c426baf609e2ce0b919a415cece70d0387199a1260e82ce1c8352c6d4` | `test/goldens/goldens/onboarding_select_390x844.png` | +| `246:90` | add | `docs/design/review/figma/onboarding-2-20260925.png` | `51f0d329717347fd0df91b6b6756fa40112c2809da65511fb1fe688d4522d1c1` | `test/goldens/goldens/onboarding_add_390x844.png` | +| `246:113` | selected | `docs/design/review/figma/onboarding-3-20260925.png` | `d89d8e494229a0e62ed43054028b7f1a46ac23017cc646dac83088456e18cdca` | `test/goldens/goldens/onboarding_selected_390x844.png` | +| `246:150` | order | `docs/design/review/figma/onboarding-4-20260925.png` | `79514dd77bc8bcf081cf8dfd3e813baf92e256c00715fdba94c85d2a26055f81` | `test/goldens/goldens/onboarding_order_390x844.png` | +| `246:180` | time | `docs/design/review/figma/onboarding-5-20260925.png` | `681613261674d17d72260f67bc17f63574bebe11ac3bf9b8747d190b84fbc489` | `test/goldens/goldens/onboarding_time_390x844.png` | +| `246:212` | buffer | `docs/design/review/figma/onboarding-6-20260925.png` | `0b148e6e8b357f3b41e298e508148176b183a7956b3acfc7b7135a242d81ed87` | `test/goldens/goldens/onboarding_buffer_390x844.png` | +| `278:100` | time_focused | `docs/design/review/figma/onboarding-7-20260925.png` | `389a73aa5b3fc1a3efec3077fd94695009a2f5ce5d00ec14469614a377bed3a8` | `test/goldens/goldens/onboarding_time_focused_390x844.png` | +| `278:187` | time_short | `docs/design/review/figma/onboarding-8-20260925.png` | `9a9ec8a31e93b79045623e66c7a01622bde52dcd0ddc89b15b2dd9ce3df7390b` | `test/goldens/goldens/onboarding_time_short_390x844.png` | +| `278:274` | time_long | `docs/design/review/figma/onboarding-9-20260925.png` | `bdceb130364cf9a585bc47426462460e408f24319290c131dfbce0b5637d9222` | `test/goldens/goldens/onboarding_time_long_390x844.png` | +| `246:230` | notification_permission | `docs/design/review/figma/onboarding-10-20260925.png` | `ceba9ea9bb5429e33ac8bd38332f90c93ca65878c60ae3d707e8a12a3e2e1b5e` | `test/goldens/goldens/notification_permission_390x844.png` | +| `246:238` | system_prompt | `docs/design/review/figma/onboarding-11-20260925.png` | `12bd9bd4f0c706eb1ac9de5202595e20aa746ace1ee4bc84bf98743fa1565d2d` | `OS-owned system prompt; gateway behavior tested` | + +## Changes + +- The welcome screen uses the exact downloaded greeting image, 271×280 at the source position, and a 358×58 primary action at (16,752). The content scrolls at narrower widths/larger text. +- The existing four-step setup now uses the source Korean headings, single-line STEP labels, exact source progress segments, five editable preparation suggestions, 62px selection/reorder rows, and matching primary colors. The flow remains entirely local. +- Time entry now uses an actual numeric TextFormField and platform keyboard, matching the interaction in the Figma TimeInput family. The existing Formz domain validation still rejects zero/negative/over-1440-minute durations. Fields preserve controller selection while editing and update when the cubit changes externally. The total is derived from entered durations and inherits the real app font. Tapping anywhere on a duration row focuses the field and selects its value, including the row-wide accessibility bounds reported by iOS. The unit and accessibility label use the active localization. +- The notification request uses the exact 36×37 bell SVG inside a 70px badge, 358×58 primary action with an 8px radius, and a real TextButton for the defer action. Permission results, denial, deferral, settings resume and home navigation retain the existing gateway/cubit behavior. + +## Documented exceptions + +- Native status bars, home indicators, numeric/alphabetic keyboards, and the iOS permission alert are OS-owned. Widget goldens model keyboard insets and focus but do not draw a fake system keyboard or permission prompt. `246:238` is excluded only as a platform overlay; the app-owned request behind it is reviewed against `246:230`. +- The source time input screenshots show “총 시간: 30분” even when displayed values sum to 0, 60, 40, or 100030. Flutter calculates the real sum and prevents submitting 100000 minutes. The mockup’s oversized value is an invalid-input fixture, not permission to weaken the domain limit. +- Figma shows an enabled Next action with zero preparation selections. Flutter keeps it disabled until at least one valid preparation is selected. +- The default spare time remains the current product value 30 minutes. The source 10-minute state is reproduced with a valid 10-minute fixture. The existing minimum-decrease warning is visible at the minimum; the Figma buffer frame omits this current product warning. +- Notification rationale preserves the product’s precise schedule-preparation reminder copy (“약속 준비 리마인더를 보내…”), which explains actual local-notification use. It does not revert to the older generic promise in Figma. +- The current app uses bundled Pretendard; Figma uses Noto Sans KR/IBM Plex Sans KR. Text metrics vary. STEP labels remain compact while their semantic label announces the active step; form copy, controls and input text scale normally. +- App content can scroll and grows with text. An added preparation field scrolls into view based on the actual keyboard/focus geometry; the Figma AddPreparation frame freezes a particular scroll offset. The selection form retains the same vertical position when selections change, while the source SelectedPreparation variant shifts its first row down 22px. The app avoids moving all rows when the user selects a choice. +- Notification defer retains a 44px touch target, which shifts the footer content 4px relative to the smaller source text target. Numeric inputs retain a real caret, focus underline, and platform text editing. Long invalid input expands to fit Pretendard digits rather than clipping to the mockup width. + +## Assets + +All files below are nonempty original Figma downloads. SVG root sizes are used at callsites without modification. The greeting image is precached before visual capture so missing asynchronous raster decoding cannot be accepted as a blank golden. + +- `assets/design/onboarding_drag.svg` — SHA-256 `ad38a92436c5b8aad4e799b8436f1d2560cce14091039065a86e6753c4393508` +- `assets/design/onboarding_greeting.png` — SHA-256 `b37032dbcde924bf5f054a06c105efe884d8ccef787274e6c5648dd0e46d8c64` +- `assets/design/onboarding_progress_complete.svg` — SHA-256 `9771c7f3860d3f95d9fb95b2d359f071aab7da254cbdd860b2abdeca00dd9247` +- `assets/design/onboarding_progress_current.svg` — SHA-256 `7ff87b51206a3f1c5a9bb5aafd71a68b179aef5095e215b9a9c73ee47d93386d` +- `assets/design/onboarding_progress_last_current.svg` — SHA-256 `c5281905e24fc557df43aef4e3f55767597a875d3f9466c1ea92f4cf2ca18f45` +- `assets/design/onboarding_progress_last_pending.svg` — SHA-256 `329c31f798bace191fea4bcf6044c812a023ea9e8197625ec9a6084b15cd15dd` +- `assets/design/onboarding_progress_pending.svg` — SHA-256 `8c43a3af95906abc736c744e41de6708a20456f928ee6354b59c4cc24da4cf7c` +- `assets/design/notification_bell.svg` — SHA-256 `cda453a5be09d508d0287b623ad140dd1c298d7211fb1283b0efba3fe100ac2d` + +## Verification + +New tests cover the welcome image/action geometry; initial selection validation; selecting, adding and editing six preparations; reorder persistence; numeric focus/short/oversized entry; computed totals; invalid submission prevention; valid duration persistence; 320px/2× text; and notification request golden/iOS touch and label checks. Existing tests cover local profile submission/failure and every notification gateway result. The three-file focused run completed with **17 tests passed** (`/private/tmp/ontime-onboarding-final2-test.log`). The final five visual tests, including the added row-center tap → focus and full-selection regression, passed after the font and drag-size refinements (`/private/tmp/ontime-onboarding-final3-test.log`). A five-test non-update golden run also passed before the final invalid-input width adjustment; the parent task owns the final full-suite verification. + +The parent task independently rebuilt the iOS app and confirmed row-wide tap, entry of 3 minutes, and onboarding save on the simulator after this change. This is simulator evidence, not physical-device evidence. + +Final visual assessment: all 11 app-owned states have source-backed captures and are reviewed with the explicit behavior, typography, and OS exceptions above. The greeting raster, progress segments, row styling, drag icon, bell, actions, recovery-related states and totals render correctly. This is not a claim of pixel-identical Figma fonts or fake native keyboards. No unresolved app-owned icon or clipping defect is accepted as a passing golden. diff --git a/docs/design/review/original-form-parity-20260925.md b/docs/design/review/original-form-parity-20260925.md new file mode 100644 index 00000000..3324563c --- /dev/null +++ b/docs/design/review/original-form-parity-20260925.md @@ -0,0 +1,41 @@ +# Original appointment forms and preparation editing — 2026-09-25 + +Canonical Figma file: `FIdR6bUMHScn9FWbwqrBsA`, page `1:26`. Fresh design context and screenshots were read for all five nodes before changing the implementation. The source exports below are stored at 390×844. Flutter captures use the real production widgets and bundled Pretendard fonts. + +| State | Figma node | Source PNG | Flutter golden | Source SHA-256 | +|---|---|---|---|---| +| Name input | `251:42` | `figma/form-name.png` | `form_name_390x844.png` | `912f174f618dfd88e6bdec1ad0f1aca2ae403377e51fcbb9ddb234c8f6642965` | +| Date picker | `251:75` | `figma/form-date.png` | `form_date_picker_390x844.png` | `147cc7d396469cb4e11af53b1dca3c25c1d7da57cf75c2e5a315143eb6bdb616` | +| Time picker | `251:119` | `figma/form-time.png` | `form_time_picker_390x844.png` | `50aacc58ce78d7ed1c2e1ccb027260cf1cca8e7e17bd1e8e2f17feb638b64995` | +| Place and travel input | `251:162` | `figma/form-place.png` | `form_place_390x844.png` | `2bb089c948342412305999ccc5303fecf43c751b3ccd0dcd7caa88acfd40996e` | +| Preparation edit | `1334:2023` | `figma/form-preparation.png` | `preparation_edit_390x844.png` | `0ea7a3def11759115f924c88cc93b9f4176c52a09a40876c4d2bd4ded1f30684` | + +Source paths are relative to this report; Flutter goldens are under `test/goldens/goldens/`. + +## Implementation and comparison + +- Name and place captures open `KeyboardBackedBottomSheet` through the same `showModalBottomSheet` options used by production, then render/navigate the actual `ScheduleMultiPageForm`. They cover the empty name and empty place/zero travel state. The static bloc isolates rendering; existing form widget/domain tests cover validation and save behavior. +- The shared date/time/minute picker sheet now follows the source structure: 16px horizontal padding, 24px top/section gaps, 167px native wheel viewport, 16px top corners, 42% scrim, and paired source-color cancel/input buttons. Date/time/minute values still come from native Cupertino widgets. Save commits the current value; cancellation only dismisses. The minute scroll controller is disposed after the modal completes. +- Preparation editing now has a meaningful localized header and Done action, a computed total duration, 16px horizontal content insets, and list start at y167. Four rows retain the source 62px height and 8px separation at default text scale. The source drag icon is stored as `assets/preparation_edit_drag.svg` (18×16, SHA-256 `5755366e0fe2044409763c277fea35bd8fd2d8f55084bdac3b0ce4596885fc48`). Duration chips grow with text scale and show localized minute units. The existing draft/save/reorder interactions remain in place. +- The 10+10+5+5 minute fixture displays a real 30-minute total. Editing the first step and pressing Done updates `PreparationEditDraftCubit`; the same save remains reachable at 2× text scale. + +## Explicit source exceptions + +These five states are reviewed with exceptions, not pixel-identical renders: + +1. **Current shared form flow:** The original name/place/date/time Figma nodes contain an 80% sheet, `STEP 1–4` labels, and a top-right Next action. Current production and the newer recurring design use an 85% sheet, Korean descriptive step labels, and bottom Back/Next actions. That current shared flow is preserved; old source frames do not justify reverting the newer recurring implementation. Name/place content remains the actual editable fields, including useful name placeholder and the real `0시간 0분` value. Source empty placeholders are not application data. +2. **Native picker rendering:** Figma contains flat numeric wheel artwork, while Flutter retains accessible, localized Cupertino wheels, including Korean year/month/day and AM/PM labels, native perspective, and selection behavior. The Figma sheet starts at y532 but its declared 333px content extends 21px beyond the 844px frame. Flutter keeps the entire sheet and bottom controls visible, so its top is y513. The isolated date/time golden's host above the modal is only a test trigger; comparison is the picker sheet region, not that host. The app's actual date/time field integration is covered separately by `schedule_date_time_form_test.dart`. +3. **Typography:** Source uses Noto Sans KR/IBM Plex Sans KR; the app uses Pretendard. The golden harness explicitly substitutes Pretendard for unavailable private Cupertino SF fonts. Production keeps the native Cupertino font choice. Native system chrome is excluded. +4. **Preparation source content:** The Figma header says “캘린더” and shows four `00 분` placeholders beside a 30-minute total. Production uses “준비 과정 및 시간 수정” and the actual sum of actual step durations. This is a source text/arithmetic inconsistency, not an application mismatch to copy. The existing add control retains its 44px accessible tap area around a 32px circle; its center is 4px above the source artwork. + +## Verification + +`test/presentation/schedule_create/original_form_visual_test.dart` adds five PNG captures and six behavior/visual tests: name/place navigation, native date/time save, 2× picker cancellation without save, and preparation edit/draft save at 2×. The rendered PNGs were opened and visually compared with fresh Figma exports; this check caught and corrected the source SVG package-path issue. + +Focused original-form/picker/preparation/date/place run: 28 tests, initially 27 passed. The one failure was an existing Korean date-format assertion after the recurring design changed formatting to include weekday (`2026년 5월 15일 (금)`); the expectation was updated and all three date-time tests passed on rerun. The original-form and preparation-row rerun passed all 12 tests. After the SVG package-path correction, all six original-form tests passed again and the final preparation-edit image was inspected with the source drag icons visible. Coverage includes minute-wheel movement/save/disposal, cancel without save, timer/date initial selection save, field validation, focus identity changes, add-row picker lifecycle, reorder, and date/time cubit wiring. The root full-suite run remains the combined regression gate. + +No native-device picker or accessibility-screen-reader claim is made by these widget/golden checks. + +### Shared six-row editor regression review + +The root full-suite run exposed a 1.75% difference in the existing `preparation_spare_time_edit_390x844.png`. Comparing its test/master/diff images against `figma/preparation-spare-time-edit-default.png` (node `600:35201`) isolated the difference to the new source 18×16 drag SVG and the resulting slight name-position shift. Duration chips, row geometry, and actions were unchanged; the fresh icon and name position are closer to the source. The single affected golden was therefore updated with this evidence. All eight tests in `preparation_spare_time_edit_screen_test.dart` passed, including edit persistence during spare-time changes, save failure, asynchronous save completion, changed-duration submission, six-row geometry, and adding a seventh step. diff --git a/docs/design/review/recovery-parity-20260925.md b/docs/design/review/recovery-parity-20260925.md new file mode 100644 index 00000000..b0579e34 --- /dev/null +++ b/docs/design/review/recovery-parity-20260925.md @@ -0,0 +1,56 @@ +# Recovery, local data, and privacy visual review — 2026-09-25 + +Source file: `FIdR6bUMHScn9FWbwqrBsA`. All listed nodes were fetched with fresh high-fidelity design context and screenshots on 2026-09-25; source exports are separate from Flutter goldens. The comparison viewport is iOS 390×844, safe area top44/bottom21, Korean, text scale1. + +## Source and capture inventory + +| Figma node | Source export | SHA-256 | Flutter golden | +| --- | --- | --- | --- | +| `1957:11762` | `docs/design/review/figma/recovery-v3.png` | `0458b76579f000c3ed4b40059836b04ed35e38797caddca79477b47a75c73ea4` | `test/goldens/goldens/recovery_default_390x844.png` | +| `2003:12059` | `docs/design/review/figma/recovery-confirm-v3.png` | `ce978101bf343b568497e4c381a9e461abfbc767a50cda7268af28a7231db784` | `test/goldens/goldens/recovery_reset_confirmation_390x844.png` | +| `2005:12036` | `docs/design/review/figma/recovery-busy.png` | `93b54f8e6116d4d793d98106eb180ba920109a8f09046804a0fe2bdc2e7083d3` | `test/goldens/goldens/recovery_busy_390x844.png` | +| `1963:11762` | `docs/design/review/figma/reset-complete-v3.png` | `f98c8ab46909ad90ea9a602c8b768a7bd78fd819bb18f1bdc4f70a086230fd01` | `test/goldens/goldens/reset_complete_390x844.png` | +| `1950:11759` | `docs/design/review/figma/privacy-v3.png` | `88f84355c58e153300ccd4cce29c235c6fa3d05309fde7ba1eed99c04a6e94a5` | `test/goldens/goldens/privacy_policy_390x844.png` | +| `1978:11791` | `docs/design/review/figma/my-data-restore-preview.png` | `ffb62bd9764f8897e14d95f6f12334fd0be6f8ec0a30f31c615fd37ad68a5689` | `test/goldens/goldens/my_data_restore_preview_390x844.png` | +| `1979:11816` | `docs/design/review/figma/my-data-password-create.png` | `7e04e15b88d878162767d910c9d45f3703a2fbe6bf27d56522204cef66abb04b` | `test/goldens/goldens/my_data_password_create_390x844.png` | +| `1980:11841` | `docs/design/review/figma/my-data-password-input.png` | `94dd76f87a6a7fe77bd9672bdb645f52aa91700e48b2eeab101fbf2f4507f0c1` | `test/goldens/goldens/my_data_password_input_390x844.png` | +| `1981:11866` | `docs/design/review/figma/my-data-password-invalid.png` | `de016130f45b290c63baddad21479e1ed2b65de2701be604bd8432c55d67ec9f` | `test/goldens/goldens/my_data_password_invalid_390x844.png` | +| `1982:11891` | `docs/design/review/figma/my-data-password-mismatch.png` | `894f3c951dfcb852e4f42b96c40cd19586434ac3eecda4aad3f8b5211f7f4621` | `test/goldens/goldens/my_data_password_mismatch_390x844.png` | + +The same `2003:12059` confirmation is used by My Data; its route capture is `test/goldens/goldens/my_data_reset_confirmation_390x844.png`. + +## Implementation and comparison + +- Recovery restores the exact 68×68 warning SVG, 22×22 information SVG, 352px content width, notice at (19,360), retry at y457, and fallback panel at y578. Both actions have 44px touch targets. Retry dispatches the existing auth retry event; reset still requires explicit confirmation. +- Reset confirmation uses the exact 47×47 destructive-warning and 22×22 information SVGs. Consequences start at (19,254); external-backup notice starts at (19,386); cancel/delete actions sit at y742. Both My Data and recovery open this shared screen. +- Busy recovery uses the exact 40×40 spinner artwork inside a 47px slot, heading at y397, disabled 356×44 actions at y702/y756. The spinner animates in production; the golden fixes its animation phase. A failed reset clears busy state and presents an error. +- Reset completion restores the exact 69×69 success and 23×23 outlined-information artwork. The final action remains at y754 with 50px height. The app retains required restart guidance after storage has been closed. +- Privacy uses the source 358px card grid at x16, first card y169, 14px inner padding, 16px section-number column, 12px gutter, six complete selectable sections, and scrollable large text. +- Password creation, restore password, invalid-length, mismatch, and restore preview are compared with all five fresh source exports. The 277px card at y352 is preserved; the primary action now uses the source #4F69DF through the existing scoped RefreshTheme, without changing global button styling. Secure inputs, 15–128 character validation, mismatch protection, cancellation, preview metadata, keyboard and 2× text behavior remain tested. + +## Explicit runtime and source exceptions + +- Native status bar and home indicator are operating-system UI and are excluded from app goldens. +- Bundled Pretendard remains the app font; Figma uses Noto Sans KR/IBM Plex Sans KR on these screens. Glyph shape and line wrapping differ, notably one line of the privacy backup section and password bullets. Cards grow naturally at larger text sizes rather than clipping the fixed Figma frames. +- Recovery and reset-complete are terminal storage states: their Figma back chevron has no valid destination, so the app omits it. Confirmation and privacy have a real 44px back control. The privacy source has a 37px header; the app keeps a 44px touch target and compensates introductory spacing to retain the first card at y169. +- The Figma recovery buttons are 43px/42px; the app uses 44px accessibility targets and compensates the adjacent gaps. +- After destructive reset, the database, secure key, and services are closed. Figma says “시작하기” and claims immediate restart; app keeps “다시 시작 안내” and explains a full process restart. The action displays guidance instead of accessing closed storage or bypassing initialization. This is a deliberate lifecycle requirement, not unfinished layout work. +- The five Figma My Data overlays contain a Restore-row icon/text overlap and a Reset-row label copied from backup export. Flutter retains correct current row labels, icons, and nonoverlapping layout underneath the scrim. +- Password fields retain actual secure TextField behavior, focus, keyboard handling, and scalable text; the mockup’s literal bullet strings are not used as fake fields. + +## Asset verification + +All added SVGs are nonempty files downloaded from the specific Figma context asset URLs. Original root dimensions are preserved at the callsites. Existing recovery_warning.svg and reset_success.svg match the fresh downloaded source byte-for-byte. The busy spinner is stored separately because its fresh source hash differs from the previous generic spinner. + +- `assets/design/recovery_warning.svg` — SHA-256 `79f8587b1905dcf7a17eea7d1876d7222ea1dc5f08193464d08dbceed3b8144e` +- `assets/design/reset_success.svg` — SHA-256 `6523bd1c81ab2cf36aa8522260900bc226701babb6304d17c2f9d758f8b3f587` +- `assets/design/information_filled.svg` — SHA-256 `af35a5afb451a5cc45b1e560d2c6922897227b119e6d09ce04e74c80ff4b491a` +- `assets/design/information_outline.svg` — SHA-256 `d32455d3d0aa53f71266ea404872bfc892c258553f649d6cedd00c56d7bebcdb` +- `assets/design/destructive_warning.svg` — SHA-256 `5e7479a845a8961b1e76d60f52270fafed5f77657c266707b6d33e5bc6c017f7` +- `assets/design/recovery_spinner.svg` — SHA-256 `3435d4eaf4260f838229014a4297393e60f3c275b299191560fc14bb99516790` + +## Verification + +Focused command: `flutter test --no-pub --concurrency=1 --update-goldens test/presentation/startup/local_data_recovery_visual_test.dart test/presentation/startup/local_data_reset_complete_visual_test.dart test/presentation/my_page/privacy_policy_screen_visual_test.dart test/presentation/my_page/my_data_screen_visual_test.dart`. Focused run: **31 tests passed**. The recovery/My Data/privacy cases passed again in the combined focused run after the header chevron refinement. Fresh captures were visually inspected against the source images. Final assessment is source-reviewed with the explicit font, platform and storage-lifecycle exceptions above; it is not a claim of pixel-identical fonts. The parent task records the final full-suite and native backup verification. + +Additional coverage: exact notice positions, destructive-screen placement, cancel leaves data untouched, retry behavior, busy disabled actions/reset failure, restart guidance, six privacy sections/2× scrolling, password validation/cancel, restoration preview cancellation, keyboard/large text, and iOS tap/label checks. diff --git a/docs/design/review/recurring-parity-20260925.md b/docs/design/review/recurring-parity-20260925.md new file mode 100644 index 00000000..28f6d835 --- /dev/null +++ b/docs/design/review/recurring-parity-20260925.md @@ -0,0 +1,66 @@ +# Recurring UI review — 2026-09-25 + +Canonical Figma file: [OnTime Design System](https://www.figma.com/design/FIdR6bUMHScn9FWbwqrBsA/OnTime-Design-System). All 18 nodes below were read with fresh `get_design_context` and `get_screenshot` results before implementation, and compared with rendered Flutter fixtures. Original exports are preserved with SHA-256 hashes. These are reviewed implementations with the explicit adaptations below; this report does not assert zero-pixel difference from Figma. + +## Source-to-fixture inventory + +Each source export and golden is 390×844 at device pixel ratio 1. The standalone sheet fixtures reserve real safe-area space (44 top/34 bottom); they do not paint a fake system status bar. Creation fixtures exercise the actual four-step form and use future dates against an injected clock to keep the database assertions deterministic. `date_time` and `preparation` come from `recurring_form_integration_test.dart`; the other 16 come from `recurring_parity_visual_test.dart`. + +| State | Figma node | Source PNG | Source SHA-256 | Flutter golden | +|---|---|---|---|---| +| `date_time` | `2112:2394` | `docs/design/review/figma/recurring-date-time-20260925.png` | `e81a92bc149ec372cf5440f1a15992a70af63509f97c01b6f009df2bb2c7bbdd` | `test/goldens/goldens/recurring_date_time_390x844.png` | +| `weekly` | `2112:21323` | `docs/design/review/figma/recurring-weekly-20260925.png` | `343688b5181b27e682b857de98fe8a907e6900521acaceb97f2e9c6e5891c82f` | `test/goldens/goldens/recurring_weekly_390x844.png` | +| `monthly` | `2112:21352` | `docs/design/review/figma/recurring-monthly-20260925.png` | `b69b5931f22f8893e31338cfac15eb2c15b37be6c06b2864d787113e480e974b` | `test/goldens/goldens/recurring_monthly_390x844.png` | +| `ending` | `2112:21395` | `docs/design/review/figma/recurring-ending-20260925.png` | `22c1608681bd101b922d4deb4a1c1a0469cd15465a98d4d76d552a68be9b3688` | `test/goldens/goldens/recurring_ending_390x844.png` | +| `preparation` | `2112:21456` | `docs/design/review/figma/recurring-preparation-20260925.png` | `ecb0df74ebe0e54067514f16be2d2a25a1dd1ccdec5bdc4b0073fd0107b59fe3` | `test/goldens/goldens/recurring_preparation_390x844.png` | +| `review` | `2112:21500` | `docs/design/review/figma/recurring-review-20260925.png` | `29a84cd7e21f2b9369967d8f26b9fcf1c25f20329ce584242b3db911cbe004fe` | `test/goldens/goldens/recurring_review_390x844.png` | +| `conflicts` | `2112:21531` | `docs/design/review/figma/recurring-conflicts-20260925.png` | `c4d4be1b26f8f51628b55660a7eaa3f2cb9847d4b23b7f39658e9a52bb72f7fe` | `test/goldens/goldens/recurring_conflicts_390x844.png` | +| `persistent_conflict` | `2112:21557` | `docs/design/review/figma/recurring-persistent-conflict-20260925.png` | `eed0e8720e4242851e2f518eb43ed98f44d0a991e15287aecd0ae6dd60474a00` | `test/goldens/goldens/recurring_persistent_conflict_390x844.png` | +| `scope` | `2112:21588` | `docs/design/review/figma/recurring-scope-20260925.png` | `6361ec89504c0d9deea00e69b22bdde1dd2ec6f7b10f816381e7ea8c55b245e5` | `test/goldens/goldens/recurring_scope_390x844.png` | +| `detached` | `2112:21613` | `docs/design/review/figma/recurring-detached-20260925.png` | `d74e3d7294dbc3553fea3d445c7b1082a38a65eb1aab22f1a3d3cea136c1e5f1` | `test/goldens/goldens/recurring_detached_390x844.png` | +| `time_exceptions` | `2112:21635` | `docs/design/review/figma/recurring-time-exceptions-20260925.png` | `44c32c26792c5a4838587e221c1287eca2f3aaa1ecfd5f7b41462f198b13dabe` | `test/goldens/goldens/recurring_time_exceptions_390x844.png` | +| `save_error` | `2112:21659` | `docs/design/review/figma/recurring-save-error-20260925.png` | `19ceb72970e68136567e80473a5439485cab149f89d8f7d81b4043eafd1bd067` | `test/goldens/goldens/recurring_save_error_390x844.png` | +| `frequency` | `2113:21627` | `docs/design/review/figma/recurring-frequency-20260925.png` | `b9e75a77c132f8b8e7995878160f1c329e655e40262617c9ef91b4096cac1357` | `test/goldens/goldens/recurring_frequency_390x844.png` | +| `monthly_fields` | `2113:21670` | `docs/design/review/figma/recurring-monthly-fields-20260925.png` | `657b3d1316485837fa641d87754d571060845e240c353a3ee9cf37ae4a2d405b` | `test/goldens/goldens/recurring_monthly_fields_390x844.png` | +| `end_date` | `2113:21708` | `docs/design/review/figma/recurring-end-date-20260925.png` | `5ec407e1334d1e0b012647fb172fbf30e249b606f8600c1c486c8ad857af9ae7` | `test/goldens/goldens/recurring_end_date_390x844.png` | +| `end_confirm` | `2113:21745` | `docs/design/review/figma/recurring-end-confirm-20260925.png` | `17fe56d97a952bcec836cd2fb6c9451eebdf10d2961819c1f729366979cec970` | `test/goldens/goldens/recurring_end_confirm_390x844.png` | +| `detail` | `2112:21715` | `docs/design/review/figma/recurring-detail-20260925.png` | `2d2dce2fd2043e9d091cab354aa346604a8d52db71eb131e075814a15d6de87e` | `test/goldens/goldens/recurring_detail_390x844.png` | +| `occurrence` | `2112:21768` | `docs/design/review/figma/recurring-occurrence-20260925.png` | `31f7f534b0b8ccf5cde3c58b5027b85d40175bdce29d6939b0cfbd4a3c9c6d26` | `test/goldens/goldens/recurring_occurrence_390x844.png` | + +## Implemented changes + +- Used source SVG assets for the recurrence calendar/clock/repeat icons, selection radio controls, preparation handles/underlines/chevrons, review notices, occurrence controls, and detail actions. The source assets are under `assets/recurrence_*.svg`. +- Matched the weekly segmented selection, monthly interval and all four monthly modes, selection panels, compact end-count field, and underlined inclusive end-date field. Day-of-month and ordinal-weekday inputs retain validation and the ability to switch patterns. +- Updated review, conflicts, persistent conflicts, detached occurrences, edit scope, civil-time ambiguity, save failure, detail, and end confirmation compositions. Conflict selection remains explicit, changes the saved count, and blocks saving unresolved or empty selections. +- Kept the actual name → date/time → place/travel → preparation wizard. Date/time and preparation content use the new visual styles. Buffer time stays outside total preparation time; recurrence preparation continues to be a copy owned by the series. +- Kept the existing recurring management and empty-state implementation. Only its detail and end-confirmation children changed. The detail end action is visible above the footer at the target viewport, and remains scrollable with more preparation steps. +- Preserved nonzero minutes in the occurrence title and fixed the monthly dropdown to inherit the application font instead of leaving its selected label with an unspecified font. + +## Explicit runtime and visual adaptations + +1. **Typography and system chrome:** the Figma nodes mix Noto Sans KR / IBM Plex Sans KR and contain a drawn iOS status/home bar. Flutter keeps the bundled Pretendard app typography, native safe areas, and real modal presentation. The detail, occurrence scope, and end confirmation are bottom sheets and therefore expose the actual dimmed backdrop at the top. Font metrics, paragraph wrapping, text antialiasing, and some resulting vertical spacing differ. Goldens load Pretendard and MaterialIcons deterministically; they are regression baselines, not screenshots of the OS chrome. +2. **Creation shell and fixture data:** source date/time and preparation nodes depict different step labels and sheet positions. The existing four-step route, navigation, validation, keyboard handling, and travel step are preserved. Their integration goldens show that real form in a full-screen test host, with a daily three-occurrence rule in 2030 and one 25-minute default step plus five minutes of buffer. The source sample uses a 2026 weekly rule, two steps totalling 25 minutes, and ten minutes of buffer. These values are fixture inputs, not hard-coded production substitutions. The date/time summary defers the actual first valid occurrence until preparation is known, then review uses the recurrence engine result. +3. **Touch targets and scrolling:** weekday controls remain 44×44 even where the static source is smaller. Recurrence pages scroll above fixed actions; lengthy conflict lists may require scrolling to the final explanatory notice. The weekly screen retains an explicit frequency/disable link so a recurring draft can become a one-off schedule. Expanded text can increase panel heights. +4. **Correct recurrence dates:** review and conflict fixtures expand the weekly rule with the real engine and retain actual weekday/date correspondence, count rules, skipped dates, preparation cutoff, and inclusive end date. They do not copy dates that conflict with the selected rule. Finite rules also keep their end/count label in detail. +5. **Unavailable source-only values:** a detached `ScheduleEntity` does not carry its preparation duration, so the detached card renders its real date/time without inventing the Figma sample's 40-minute preparation. Likewise, `RecurrenceConflict.other` has no preparation-start timestamp, so persistent conflicts show the other appointment's actual time rather than claiming a complete reserved interval from incomplete data. The edited occurrence has an actual override notice; it cannot infer a full series rule from an isolated entity. The detail preparation introduction is generic to the series, while row names and durations are actual values. +6. **DST and failure states:** the repeated-time sheet displays offsets resolved for the supplied civil date and zone (fixture: America/New_York, 2026-11-01 01:30, UTC−04:00 and UTC−05:00). It does not display a fictitious form-progress rail. The Figma failure frame combines an empty-generation example and a storage-error example; runtime renders the single actual error it received and keeps retry behavior. The storage fixture therefore does not falsely claim there were no matching occurrences. + +The source and fixture pairs above were manually reviewed for content, state, controls, colors, spacing and wrapping. The adaptations are intentional boundaries of this review; `reviewed` must not be presented as an assertion that every rendered pixel equals Figma. + +## Verification + +- `dart analyze lib/presentation/recurring lib/presentation/schedule_create/schedule_date_time/screens/schedule_date_time_form.dart lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart test/presentation/recurring/recurring_parity_visual_test.dart` — no issues. +- Focused run of `recurring_parity_visual_test.dart`, `recurring_screens_test.dart`, `refreshed_screens_test.dart`, and `recurring_form_integration_test.dart` with `--no-pub --concurrency=1 --update-goldens` — **32 tests passed**, 12 seconds. The initial run exposed a detail end-action hit-test problem; spacing was corrected and all 32 passed after correction. +- Final follow-up: `recurring_parity_visual_test.dart` — **13 passed with `--update-goldens`**, then **13 passed without updating goldens**, two seconds each. The monthly day label renders with Pretendard after its font inheritance fix; detail uses a generic series preparation introduction and monthly field focus is settled before capture. All revised images were re-opened for visual verification. +- Coverage includes weekday/count validation, explicit conflict exclusions, persistent-conflict prevention, local database writes only after review confirmation, owned preparation, alarm effects, cancelled review, retry after invalid preparation, and following edits preserving individual overrides. Existing settings/review checks exercise 1.6× text. +- This subtask did not independently claim simulator/physical-device execution. Native execution evidence belongs to the parent task's separate runtime report. + +## `/scheduleEdit/:scheduleId` mapping + +`ScheduleEditScreen` loads `ScheduleFormEditRequested(scheduleId, scope)` and reuses the exact `ScheduleMultiPageForm` used by `ScheduleCreateScreen`; it submits `ScheduleFormUpdated` instead of `ScheduleFormCreated`. Its visual component mapping is therefore shared with the creation nodes, including `2112:2394` date/time and `2112:21456` preparation, plus the scope/review nodes above. No separately verified Figma edit-only node is asserted here. + +The integration case `following edit loads the series base and preserves an individual name override` loads an actual saved series from the database, edits from the second occurrence, verifies the two-occurrence review and preserves the individual name override after confirmation. This is behavioral edit-mode coverage; it is not a separate edit-route screenshot. The original name/date-picker/time-picker/place-travel nodes and `1334:2023` preparation-edit are audited by the calendar/original-form subtask rather than duplicated here. + +## Native backup review boundary + +Read-only review covered `BackupFilePicker`, `BackupService.exportToUserSelectedFile`, `OnTimeBackupExporter` in `ios/Runner/AppDelegate.swift`, their tests, and the installed file-selector iOS delegate implementation. No blocking production defect was found in the reviewed diff. The review confirmed encrypted bytes only, filename/path validation, a concurrent-export guard, explicit cancel/dismiss handling, temporary-directory cleanup, and marking only the captured revision after a completed save. This code review is not additional evidence of an executed native save/restore flow. diff --git a/docs/design/review/runtime-parity-20260925.md b/docs/design/review/runtime-parity-20260925.md new file mode 100644 index 00000000..0bea73c3 --- /dev/null +++ b/docs/design/review/runtime-parity-20260925.md @@ -0,0 +1,26 @@ +# Preparation runtime comparison — 2026-09-25 + +Canonical file `FIdR6bUMHScn9FWbwqrBsA`, Preparation page `1:21`. Fresh design contexts and screenshots were compared to real-font Flutter captures at Korean 390×844, top safe area44, fixed time and preparation data. + +| Node | Source image | Flutter golden | +| --- | --- | --- | +| `118:2` | `docs/design/review/figma/runtime-on-time.png` | `test/goldens/goldens/runtime_on_time_390x844.png` | +| `118:58` | `docs/design/review/figma/runtime-late.png` | `test/goldens/goldens/runtime_late_390x844.png` | +| `118:114` | `docs/design/review/figma/runtime-finish-confirm.png` | `test/goldens/goldens/runtime_finish_confirm_390x844.png` | + +The live timer uses the source268px circle at y121, 14px stroke, 28px preparation name, 48px countdown, top426px/bottom418px layout, blue/red timing colors, 358px preparation cards, 34px numbered badges and source checkmark artwork. Active cards135px, inactive62px, 14px connectors, 53px skip and57px finish actions match the reference geometry. Actual duration values, progress, scrolling and text scaling remain dynamic. Restoring an in-progress preparation now draws its saved ring progress immediately instead of starting at zero. + +Manual finish now opens the explicit source confirmation. Cancel leaves the session running; only confirm dispatches the finish event. Lateness is recalculated at confirmation, and the existing persisted result flow is retained. Automatic all-steps-complete and continuing-until-departure behaviors are separately regression-tested. + +A final independent review found that the last timer tick could open an automatic prompt over an already-open manual prompt. Both paths now reserve the same dialog slot and recheck the active schedule when the dialog closes. Continuing after the last step acknowledges completion without immediately reopening the automatic prompt; continuing before completion still allows the later automatic prompt. Two regressions use the real ScheduleBloc and a five-second last-step timer to prove both transitions. The final focused runtime group passes all 32 tests without changing the goldens. + +## Explicit exceptions + +- Figma's ring names “메이크업” while the active card names “샤워하기”, duplicates some step numbers and displays unrelated ring progress/countdowns. Flutter derives all names, numbers, durations and progress from the same active preparation state. The late fixture keeps real active-step remaining time rather than replacing it with a static overdue duration. +- Source confirmation promises Home navigation; the actual app shows the preparation result. Confirmation copy accurately describes that behavior. A functional44px Home close control is retained. +- Native status/home indicators are outside widget captures; bundled Pretendard replaces the Figma font. The dialog uses the viewport center while its text grows naturally. +- At320×640 and2× text, card content scrolls so the skip action stays reachable and the finish action remains anchored. Geometry from the fixed source is not enforced when it would clip accessible text. + +## Verification + +Five new tests cover three compared goldens, exact ring bounds, live remaining time, manual cancellation/confirmation, narrow enlarged-text skip/finish and restored initial progress. Twenty-five existing preparation flow tests cover early start, navigation, late/on-time completion, continuing timers and stale/missing schedules. All pass after the source changes; final combined validation is recorded in the completion report. The core backup checks run alongside this focused group are reported separately. diff --git a/handoff/ios-ui-quality-figma-parity.md b/handoff/ios-ui-quality-figma-parity.md new file mode 100644 index 00000000..3eadad13 --- /dev/null +++ b/handoff/ios-ui-quality-figma-parity.md @@ -0,0 +1,23 @@ +# iOS UI quality / Figma parity handoff + +Updated 2026-09-25. Work is based on refreshed `main` `c7184b2a`, merged at `64bc111d`. The canonical source is [OnTime Design System](https://www.figma.com/design/FIdR6bUMHScn9FWbwqrBsA/OnTime-Design-System); the old `GiDCK...` link is not the source. [Draft PR #596](https://github.com/DevKor-github/OnTime-front/pull/596) remains open for review; this handoff does not authorize merge or assert release readiness. + +## Completed scope + +- [The manifest](../docs/design/figma_code_map.yaml) passes strict validation: 21 routes contain 16 reviewed/five excluded defaults, and 58 variants contain 57 reviewed/one excluded state. There are **73 reviewed app entries and zero pending/in-progress entries**. Each reviewed entry has source/golden evidence or a documented shared-component mapping. Five route exclusions mean no current full-screen Figma source; the one variant exclusion is the OS-owned permission alert. +- Home/Calendar, original and recurring appointment forms, preparation editing/runtime, onboarding/permissions, recovery/reset, My Data dialogs, and privacy have source-backed review and focused tests. The six detailed reports and their source hashes/golden links are indexed in [the completion report](../docs/design/review/completion-20260925.md#evidence-index). +- The calendar's December-title/November-grid contradiction is resolved as a documented source exception: runtime retains correct dates. Other explicit adaptations retain Pretendard, real durations/counts, the current four-step form, native controls/chrome, accessible touch targets, and reset restart guidance. None is a claim of pixel identity. +- `/scheduleEdit/:scheduleId` shares the creation form and its mapped components. Database integration verifies following edits preserve individual overrides. Original form and preparation-edit mappings were also audited; a separate edit-only Figma screen is not invented. +- [Native evidence](../docs/design/review/native-20260925/README.md) records a rebuilt iPhone17/iOS26.5 debug simulator: setup and actual numeric entry; notification/AlarmKit denial and defer; successful Files export of an 811-byte encrypted backup; changing buffer 30→35; importing/decrypting the same backup; restoring buffer to 30; reset; and full relaunch to Welcome. The external backup file's hash remained unchanged after reset. This also verifies the repaired iOS export bridge and import UTI boundary in production UI. +- The final full-suite and coverage receipt has one authoritative location: [final combined validation](../docs/design/review/completion-20260925.md#final-combined-validation). Do not reuse the old checkpoint's test/coverage totals or treat targeted passes as the final combined result. + +## Explicitly outside the completed app mapping + +The manifest's **11 unmapped `design_only_nodes`** are standalone Schedule Detail screens, with no matching app route/implementation. They are different from the existing expandable Calendar card: Upcoming, PreparationDue, Preparing, CompletedOnTime, CompletedLate, CompletedUnknown, PastUnrecorded, Loading, Refreshing, NotFound, and Failure. Their node IDs and audit evidence are in [the Calendar report](../docs/design/review/calendar-parity-20260925.md#unmapped-full-schedule-detail-design-audit). Strict app coverage does not certify or implement them; adding that product flow is separate work. + +## Remaining release acceptance + +1. Local final combined validation is complete and recorded in the completion report. Read the current PR head and its remote checks before merge; keep local execution and remote CI receipts distinct. +2. Attach an available physical iPhone and execute device font/layout inspection, VoiceOver interaction, background/locked-screen alarm delivery, and scrolling/frame-time measurements. All paired devices were unavailable during this task; debug simulator rendering is not a device performance result. +3. Keep native evidence scoped accurately. The 2026-09-25 run proves the setup/permission denial/data lifecycle above. Export cancellation has automated coverage, not an executed native cancellation receipt. Appointment creation/edit/delete and recurring scope/conflict/ownership use widget/database integration; earlier native recurring evidence retains its own date. Successful alarm delivery was not observed in this run. +4. Preserve Draft PR #596 until the required release gates and normal review are satisfied. No merge or release was performed by this documentation handoff. diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6d4ab7f5..039945cb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -40,6 +40,14 @@ "description": "The title of the calendar screen" }, "error": "Error", + "retry": "Try again", + "@retry": { + "description": "Action to retry loading failed content" + }, + "preparationInProgress": "Preparing", + "@preparationInProgress": { + "description": "Status label for an active preparation on the home card" + }, "@error": { "description": "Generic error message" }, @@ -148,6 +156,8 @@ "description": "Button text to continue preparing" }, "finishPreparation": "Finish Preparation", + "finishPreparationConfirmTitle": "Have you finished preparing?", + "finishPreparationConfirmDescription": "Finish preparation to see your result.", "@finishPreparation": { "description": "Button text to finish preparing" }, @@ -400,6 +410,18 @@ "@editDefaultPreparation": { "description": "Setting tile for editing default preparation and spare time" }, + "editPreparationSpareTimeHeader": "Edit spare time and preparation", + "@editPreparationSpareTimeHeader": { + "description": "Title of the spare-time and preparation edit screen" + }, + "done": "Done", + "@done": { + "description": "Action that saves and closes an edit screen" + }, + "addPreparationStep": "Add preparation step", + "@addPreparationStep": { + "description": "Accessible label for the preparation list add button" + }, "allowAppNotifications": "Allow App Notifications", "@allowAppNotifications": { "description": "Setting tile for allowing app notifications" @@ -428,7 +450,7 @@ "@editSpareTime": { "description": "Section title for editing spare time" }, - "editPreparationTime": "Edit preparation time", + "editPreparationTime": "Edit preparation steps and times", "@editPreparationTime": { "description": "Section title for editing preparation time" }, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 3e471b8e..5327294f 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -37,6 +37,8 @@ "notificationCleanupNeededStatus": "꺼짐 · 알림 취소 확인 필요", "calendarTitle": "캘린더", "error": "오류", + "retry": "다시 시도", + "preparationInProgress": "준비 진행 중", "noSchedules": "약속이 없어요", "setSpareTimeTitle": "여유시간을 설정해주세요", "setSpareTimeDescription": "설정한 여유시간만큼 일찍 도착할 수 있어요.", @@ -64,6 +66,8 @@ "preparationCompletedDescription": "준비 단계를 모두 완료했어요.\n지금 종료하거나 계속 준비할 수 있어요.", "continuePreparing": "계속 준비", "finishPreparation": "준비 종료", + "finishPreparationConfirmTitle": "준비가 모두 끝나셨나요?", + "finishPreparationConfirmDescription": "준비 종료를 누르면\n준비를 마치고 결과를 확인할 수 있어요.", "preparationReadyToGo": "출발 준비 완료", "signInSlogan": "당신의 잃어버린 여유를 찾아드립니다.", "signInFailedTitle": "로그인에 실패했어요", @@ -71,10 +75,10 @@ "welcome": "반가워요!", "onboardingStartSubtitle": "Ontime과 함께 준비하기 위해서\n평소 본인의 준비 과정을 알려주세요", "start": "시작하기", - "preparationOrderTitle": "앞에서 고른 준비 과정의 순서를\n알려주세요", - "preparationNameTitle": "주로 하는 준비 과정을\n선택해주세요 ", + "preparationOrderTitle": "앞에서 고른 준비 과정의 순서를\n설정해주세요", + "preparationNameTitle": "약속에 나가기 위한 준비 과정을\n선택해주세요 ", "multipleSelection": "(복수 선택)", - "preparationTimeTitle": "준비 시간", + "preparationTimeTitle": "과정별로 소요되는 시간을\n알려주세요", "addAppointment": "약속 추가하기", "next": "다음", "appointmentName": "약속 이름", @@ -128,6 +132,9 @@ "appSettings": "앱 설정", "accountSettings": "계정 설정", "editDefaultPreparation": "기본 준비과정 / 여유시간 수정", + "editPreparationSpareTimeHeader": "여유시간/준비과정 수정", + "done": "완료", + "addPreparationStep": "준비 과정 추가", "allowAppNotifications": "앱 알림 허용", "preciseNotificationPermissionRequired": "정확한 알림 권한이 필요해요", "preciseNotificationPermissionDescription": "정확한 시간에 준비를 알려드리기 위해 이 권한이 필요해요.", @@ -143,7 +150,7 @@ "logOut": "로그아웃", "deleteAccount": "회원 탈퇴", "editSpareTime": "여유시간 수정", - "editPreparationTime": "준비시간 수정", + "editPreparationTime": "준비 과정 및 시간 수정", "totalTime": "총 시간: ", "scheduleOverlapWarning": "\"{scheduleName}\"과 겹치지 않으려면 {minutes}분 안에 준비해야해요", "@scheduleOverlapWarning": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a30973c1..39a4bfd3 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -320,6 +320,18 @@ abstract class AppLocalizations { /// **'Error'** String get error; + /// Action to retry loading failed content + /// + /// In en, this message translates to: + /// **'Try again'** + String get retry; + + /// Status label for an active preparation on the home card + /// + /// In en, this message translates to: + /// **'Preparing'** + String get preparationInProgress; + /// Text shown when there are no schedules on a selected date /// /// In en, this message translates to: @@ -482,6 +494,18 @@ abstract class AppLocalizations { /// **'Finish Preparation'** String get finishPreparation; + /// No description provided for @finishPreparationConfirmTitle. + /// + /// In en, this message translates to: + /// **'Have you finished preparing?'** + String get finishPreparationConfirmTitle; + + /// No description provided for @finishPreparationConfirmDescription. + /// + /// In en, this message translates to: + /// **'Finish preparation to see your result.'** + String get finishPreparationConfirmDescription; + /// Center timer label shown after all preparation steps are complete and there is still time before leaving /// /// In en, this message translates to: @@ -818,6 +842,24 @@ abstract class AppLocalizations { /// **'Edit Default Preparation / Spare Time'** String get editDefaultPreparation; + /// Title of the spare-time and preparation edit screen + /// + /// In en, this message translates to: + /// **'Edit spare time and preparation'** + String get editPreparationSpareTimeHeader; + + /// Action that saves and closes an edit screen + /// + /// In en, this message translates to: + /// **'Done'** + String get done; + + /// Accessible label for the preparation list add button + /// + /// In en, this message translates to: + /// **'Add preparation step'** + String get addPreparationStep; + /// Setting tile for allowing app notifications /// /// In en, this message translates to: @@ -863,7 +905,7 @@ abstract class AppLocalizations { /// Section title for editing preparation time /// /// In en, this message translates to: - /// **'Edit preparation time'** + /// **'Edit preparation steps and times'** String get editPreparationTime; /// Label for total preparation time diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 3cc1ffba..1e10c9da 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -141,6 +141,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get error => 'Error'; + @override + String get retry => 'Try again'; + + @override + String get preparationInProgress => 'Preparing'; + @override String get noSchedules => 'No schedules'; @@ -228,6 +234,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get finishPreparation => 'Finish Preparation'; + @override + String get finishPreparationConfirmTitle => 'Have you finished preparing?'; + + @override + String get finishPreparationConfirmDescription => + 'Finish preparation to see your result.'; + @override String get preparationReadyToGo => 'Ready to go'; @@ -425,6 +438,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get editDefaultPreparation => 'Edit Default Preparation / Spare Time'; + @override + String get editPreparationSpareTimeHeader => + 'Edit spare time and preparation'; + + @override + String get done => 'Done'; + + @override + String get addPreparationStep => 'Add preparation step'; + @override String get allowAppNotifications => 'Allow App Notifications'; @@ -448,7 +471,7 @@ class AppLocalizationsEn extends AppLocalizations { String get editSpareTime => 'Edit spare time'; @override - String get editPreparationTime => 'Edit preparation time'; + String get editPreparationTime => 'Edit preparation steps and times'; @override String get totalTime => 'Total time: '; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 00272a0b..00432ea1 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -131,6 +131,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get error => '오류'; + @override + String get retry => '다시 시도'; + + @override + String get preparationInProgress => '준비 진행 중'; + @override String get noSchedules => '약속이 없어요'; @@ -216,6 +222,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get finishPreparation => '준비 종료'; + @override + String get finishPreparationConfirmTitle => '준비가 모두 끝나셨나요?'; + + @override + String get finishPreparationConfirmDescription => + '준비 종료를 누르면\n준비를 마치고 결과를 확인할 수 있어요.'; + @override String get preparationReadyToGo => '출발 준비 완료'; @@ -239,16 +252,16 @@ class AppLocalizationsKo extends AppLocalizations { String get start => '시작하기'; @override - String get preparationOrderTitle => '앞에서 고른 준비 과정의 순서를\n알려주세요'; + String get preparationOrderTitle => '앞에서 고른 준비 과정의 순서를\n설정해주세요'; @override - String get preparationNameTitle => '주로 하는 준비 과정을\n선택해주세요 '; + String get preparationNameTitle => '약속에 나가기 위한 준비 과정을\n선택해주세요 '; @override String get multipleSelection => '(복수 선택)'; @override - String get preparationTimeTitle => '준비 시간'; + String get preparationTimeTitle => '과정별로 소요되는 시간을\n알려주세요'; @override String get addAppointment => '약속 추가하기'; @@ -395,6 +408,15 @@ class AppLocalizationsKo extends AppLocalizations { @override String get editDefaultPreparation => '기본 준비과정 / 여유시간 수정'; + @override + String get editPreparationSpareTimeHeader => '여유시간/준비과정 수정'; + + @override + String get done => '완료'; + + @override + String get addPreparationStep => '준비 과정 추가'; + @override String get allowAppNotifications => '앱 알림 허용'; @@ -417,7 +439,7 @@ class AppLocalizationsKo extends AppLocalizations { String get editSpareTime => '여유시간 수정'; @override - String get editPreparationTime => '준비시간 수정'; + String get editPreparationTime => '준비 과정 및 시간 수정'; @override String get totalTime => '총 시간: '; diff --git a/lib/presentation/alarm/components/alarm_graph_animator.dart b/lib/presentation/alarm/components/alarm_graph_animator.dart index e0b2f57c..ffa48b27 100644 --- a/lib/presentation/alarm/components/alarm_graph_animator.dart +++ b/lib/presentation/alarm/components/alarm_graph_animator.dart @@ -31,10 +31,8 @@ class _AlarmGraphAnimatorState extends State duration: const Duration(milliseconds: 300), ); - _progressAnimation = Tween( - begin: 0.0, - end: 0.0, - ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); + previousProgress = widget.progress.clamp(0.0, 1.0); + _progressAnimation = AlwaysStoppedAnimation(previousProgress); } @override @@ -53,8 +51,8 @@ class _AlarmGraphAnimatorState extends State void _animateToNewProgress(double newProgress) { _progressAnimation = Tween( - begin: previousProgress, - end: newProgress, + begin: _progressAnimation.value, + end: newProgress.clamp(0.0, 1.0), ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); _controller.forward(from: 0); @@ -67,7 +65,7 @@ class _AlarmGraphAnimatorState extends State animation: _progressAnimation, builder: (context, child) { return CustomPaint( - size: const Size(230, 115), + size: const Size.square(268), painter: AlarmGraphComponent( progress: _progressAnimation.value, backgroundColor: widget.backgroundColor, diff --git a/lib/presentation/alarm/components/alarm_graph_component.dart b/lib/presentation/alarm/components/alarm_graph_component.dart index 988cbed7..f37845ec 100644 --- a/lib/presentation/alarm/components/alarm_graph_component.dart +++ b/lib/presentation/alarm/components/alarm_graph_component.dart @@ -18,22 +18,22 @@ class AlarmGraphComponent extends CustomPainter { final double sweepAngle = math.pi * 2; final rect = Rect.fromCenter( - center: Offset(size.width / 2, size.height), - width: size.width, - height: size.height * 2, + center: size.center(Offset.zero), + width: size.shortestSide - 14, + height: size.shortestSide - 14, ); // 채워지기 전 색 final Paint backgroundPaint = Paint() ..color = backgroundColor - ..strokeWidth = 12 + ..strokeWidth = 14 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; // 채워진 후 색 final Paint progressPaint = Paint() ..color = progressColor - ..strokeWidth = 12 + ..strokeWidth = 14 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; @@ -47,7 +47,9 @@ class AlarmGraphComponent extends CustomPainter { } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; + bool shouldRepaint(covariant AlarmGraphComponent oldDelegate) { + return progress != oldDelegate.progress || + backgroundColor != oldDelegate.backgroundColor || + progressColor != oldDelegate.progressColor; } } diff --git a/lib/presentation/alarm/components/alarm_screen_bottom_section.dart b/lib/presentation/alarm/components/alarm_screen_bottom_section.dart index 48f06ce5..6ed0644d 100644 --- a/lib/presentation/alarm/components/alarm_screen_bottom_section.dart +++ b/lib/presentation/alarm/components/alarm_screen_bottom_section.dart @@ -1,16 +1,14 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; import 'package:on_time_front/domain/entities/preparation_with_time_entity.dart'; import 'package:on_time_front/presentation/alarm/components/preparation_step_list_widget.dart'; import 'package:on_time_front/presentation/alarm/utils/preparation_step_state_mapper.dart'; -import 'package:on_time_front/presentation/shared/constants/constants.dart'; +import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; class AlarmScreenBottomSection extends StatelessWidget { - final PreparationWithTimeEntity preparation; - final VoidCallback onSkip; - final VoidCallback onEndPreparation; - const AlarmScreenBottomSection({ super.key, required this.preparation, @@ -18,105 +16,70 @@ class AlarmScreenBottomSection extends StatelessWidget { required this.onEndPreparation, }); - @override - Widget build(BuildContext context) { - final steps = List.from( - preparation.preparationStepList, - ); - - return Column( - children: [ - Expanded( - child: _PreparationStepListSection( - preparationSteps: steps, - currentStepIndex: preparation.resolvedCurrentStepIndex, - stepElapsedTimes: preparation.stepElapsedTimesInSeconds, - preparationStepStates: preparation.preparationStepStates, - onSkip: onSkip, - ), - ), - _EndPreparationButtonSection(onEndPreparation: onEndPreparation), - ], - ); - } -} - -class _PreparationStepListSection extends StatelessWidget { - final List preparationSteps; - final int currentStepIndex; - final List stepElapsedTimes; - final List preparationStepStates; + final PreparationWithTimeEntity preparation; final VoidCallback onSkip; - - const _PreparationStepListSection({ - required this.preparationSteps, - required this.currentStepIndex, - required this.stepElapsedTimes, - required this.preparationStepStates, - required this.onSkip, - }); - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Container( - decoration: const BoxDecoration( - color: Color(0xffF6F6F6), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(18), - topRight: Radius.circular(18), - ), - ), - ), - Positioned( - top: 15, - bottom: 0, - left: MediaQuery.of(context).size.width * 0.06, - right: MediaQuery.of(context).size.width * 0.06, - child: PreparationStepListWidget( - preparationSteps: preparationSteps, - currentStepIndex: currentStepIndex, - stepElapsedTimes: stepElapsedTimes, - preparationStepStates: preparationStepStates, - onSkip: onSkip, - ), - ), - ], - ); - } -} - -class _EndPreparationButtonSection extends StatelessWidget { final VoidCallback onEndPreparation; - const _EndPreparationButtonSection({required this.onEndPreparation}); @override Widget build(BuildContext context) { - final theme = Theme.of(context); final colorScheme = Theme.of(context).colorScheme; - final baseButtonStyle = theme.elevatedButtonTheme.style; - return Container( - color: Colors.white, - padding: const EdgeInsets.only(top: 16, bottom: 20), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: onEndPreparation, - style: baseButtonStyle?.copyWith( - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.disabled)) { - return colorScheme.surfaceDim; - } - return colorScheme.primary; - }), - foregroundColor: WidgetStatePropertyAll( - colorScheme.onPrimary, + return ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + child: ColoredBox( + color: AppColors.white, + child: Column( + children: [ + Expanded( + child: ColoredBox( + color: AppColors.grey.shade50, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 13, 12, 0), + child: PreparationStepListWidget( + preparationSteps: List.from( + preparation.preparationStepList, + ), + currentStepIndex: preparation.resolvedCurrentStepIndex, + stepElapsedTimes: preparation.stepElapsedTimesInSeconds, + preparationStepStates: preparation.preparationStepStates, + onSkip: onSkip, + ), + ), + ), + ), + Padding( + padding: EdgeInsets.fromLTRB( + 16, + 5, + 16, + math.max(40, MediaQuery.paddingOf(context).bottom + 12), + ), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: onEndPreparation, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(57), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + elevation: 0, + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + textStyle: Theme.of(context).textTheme.labelLarge!.copyWith( + fontSize: 18, + height: 1.4, + fontWeight: FontWeight.w500, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text(AppLocalizations.of(context)!.finishPreparation), + ), ), ), - child: Text(AppLocalizations.of(context)!.finishPreparation), - ), + ], ), ), ); diff --git a/lib/presentation/alarm/components/preparation_completion_dialog.dart b/lib/presentation/alarm/components/preparation_completion_dialog.dart index 31f31df1..2dc587db 100644 --- a/lib/presentation/alarm/components/preparation_completion_dialog.dart +++ b/lib/presentation/alarm/components/preparation_completion_dialog.dart @@ -8,16 +8,28 @@ Future showPreparationCompletionDialog({ required bool isLate, required VoidCallback onFinish, VoidCallback? onContinue, + bool manual = false, }) async { final l10n = AppLocalizations.of(context)!; final result = await showTwoActionDialog( context, config: TwoActionDialogConfig( - title: isLate ? l10n.areYouRunningLate : l10n.preparationCompletedTitle, - description: isLate + title: manual + ? l10n.finishPreparationConfirmTitle + : isLate + ? l10n.areYouRunningLate + : l10n.preparationCompletedTitle, + description: manual + ? l10n.finishPreparationConfirmDescription + : isLate ? l10n.runningLateDescription : l10n.preparationCompletedDescription, barrierDismissible: false, + useSafeArea: false, + barrierColor: Colors.black.withValues(alpha: 0.6), + innerPadding: const EdgeInsets.all(16), + titleContentSpacing: 12, + contentActionsSpacing: 12, secondaryAction: DialogActionConfig( label: l10n.continuePreparing, variant: ModalWideButtonVariant.neutral, diff --git a/lib/presentation/my_page/data_state_page.dart b/lib/presentation/my_page/data_state_page.dart new file mode 100644 index 00000000..9f518a0e --- /dev/null +++ b/lib/presentation/my_page/data_state_page.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:on_time_front/presentation/recurring/recurrence_components.dart'; + +/// Shared shell for local-data states. System bars are supplied by the OS. +class DataStatePage extends StatelessWidget { + const DataStatePage({ + super.key, + required this.title, + required this.children, + this.onBack, + this.showBack = true, + this.footer, + this.padding = const EdgeInsets.symmetric(horizontal: 19), + this.footerPadding = const EdgeInsets.fromLTRB(19, 16, 19, 33), + this.headerHeight = 44, + }); + + final String title; + final List children; + final VoidCallback? onBack; + final bool showBack; + final Widget? footer; + final EdgeInsets padding; + final EdgeInsets footerPadding; + final double headerHeight; + + @override + Widget build(BuildContext context) => RefreshTheme( + child: Scaffold( + body: SafeArea( + child: Column( + children: [ + SizedBox( + height: headerHeight, + child: Stack( + alignment: Alignment.center, + children: [ + Center( + child: Text( + title, + style: const TextStyle( + fontSize: 18, + height: 1.4, + fontWeight: FontWeight.w600, + ), + ), + ), + if (showBack) + Positioned( + left: 8, + top: 0, + bottom: 0, + child: IconButton( + tooltip: '뒤로', + onPressed: + onBack ?? () => Navigator.of(context).maybePop(), + icon: SvgPicture.asset( + 'chevron_left.svg', + package: 'assets', + width: 8, + height: 14, + ), + ), + ), + ], + ), + ), + Expanded( + child: ListView(padding: padding, children: children), + ), + if (footer != null) Padding(padding: footerPadding, child: footer), + ], + ), + ), + ), + ); +} + +class DataNotice extends StatelessWidget { + const DataNotice({ + super.key, + required this.child, + this.highlighted = false, + this.outlinedIcon = false, + this.padding = const EdgeInsets.all(17), + this.gap = 15, + this.minHeight = 0, + }); + + final Widget child; + final bool highlighted; + final bool outlinedIcon; + final EdgeInsets padding; + final double gap; + final double minHeight; + + @override + Widget build(BuildContext context) => Container( + constraints: BoxConstraints(minHeight: minHeight), + decoration: BoxDecoration( + color: highlighted + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(9), + ), + padding: padding, + child: Row( + crossAxisAlignment: outlinedIcon + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + outlinedIcon + ? 'assets/design/information_outline.svg' + : 'assets/design/information_filled.svg', + width: outlinedIcon ? 23 : 22, + height: outlinedIcon ? 23 : 22, + excludeFromSemantics: true, + ), + SizedBox(width: gap), + Expanded(child: child), + ], + ), + ); +} diff --git a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart index dba1b431..b2ea1a4c 100644 --- a/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart +++ b/lib/presentation/my_page/preparation_spare_time_edit/preparation_spare_time_edit_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/domain/entities/default_preferences.dart'; @@ -9,6 +10,7 @@ import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_pr import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart'; import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart'; import 'package:on_time_front/presentation/shared/components/two_action_dialog.dart'; +import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; class PreparationSpareTimeEditScreen extends StatelessWidget { const PreparationSpareTimeEditScreen({super.key}); @@ -76,16 +78,30 @@ class _PreparationSpareTimeEditView extends StatelessWidget { shadowColor: Colors.transparent, scrolledUnderElevation: 0, backgroundColor: Colors.transparent, + toolbarHeight: 60, + centerTitle: true, leading: IconButton( - icon: Icon( - Icons.arrow_back_ios_rounded, - color: Theme.of(context).colorScheme.outlineVariant, + tooltip: '뒤로', + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 8), + icon: SvgPicture.asset( + 'chevron_left.svg', + package: 'assets', + width: 8, + height: 14, + colorFilter: ColorFilter.mode( + AppColors.grey.shade500, + BlendMode.srcIn, + ), ), onPressed: () => context.pop(), ), title: Text( - AppLocalizations.of(context)!.editDefaultPreparation, - style: Theme.of(context).textTheme.titleLarge, + AppLocalizations.of(context)!.editPreparationSpareTimeHeader, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontSize: 19, + fontWeight: FontWeight.w600, + ), ), actions: [ BlocBuilder< @@ -101,6 +117,15 @@ class _PreparationSpareTimeEditView extends StatelessWidget { previous.isValid != current.isValid, builder: (context, preparationState) { return TextButton( + style: TextButton.styleFrom( + minimumSize: const Size(44, 44), + padding: EdgeInsets.zero, + textStyle: Theme.of(context).textTheme.titleLarge + ?.copyWith( + fontSize: 19, + fontWeight: FontWeight.w600, + ), + ), onPressed: state2.canSubmit && preparationState.isValid ? () { final currentPreparationState = context @@ -116,7 +141,7 @@ class _PreparationSpareTimeEditView extends StatelessWidget { ); } : null, - child: Text(AppLocalizations.of(context)!.ok), + child: Text(AppLocalizations.of(context)!.done), ); }, ); @@ -124,8 +149,8 @@ class _PreparationSpareTimeEditView extends StatelessWidget { ), ], bottom: const PreferredSize( - preferredSize: Size.fromHeight(33), - child: SizedBox(height: 33), + preferredSize: Size.fromHeight(17), + child: SizedBox(height: 17), ), ), body: const SafeArea(child: _PreparationSpareTimeEditBody()), @@ -240,7 +265,7 @@ class _SpareTimeSection extends StatelessWidget { child: Text( AppLocalizations.of(context)!.editSpareTime, textAlign: TextAlign.start, - style: textTheme.titleMedium, + style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), ), ), SizedBox(height: 24.0), @@ -291,7 +316,7 @@ class _PreparationSection extends StatelessWidget { child: Text( AppLocalizations.of(context)!.editPreparationTime, textAlign: TextAlign.start, - style: textTheme.titleMedium, + style: textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), ), ), SizedBox(height: 24.0), diff --git a/lib/presentation/my_page/privacy_policy_screen.dart b/lib/presentation/my_page/privacy_policy_screen.dart index c584a294..e6c4ae4e 100644 --- a/lib/presentation/my_page/privacy_policy_screen.dart +++ b/lib/presentation/my_page/privacy_policy_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:on_time_front/presentation/recurring/recurrence_components.dart'; +import 'package:on_time_front/presentation/my_page/data_state_page.dart'; class PrivacyPolicyScreen extends StatelessWidget { const PrivacyPolicyScreen({super.key}); @@ -28,60 +28,89 @@ class PrivacyPolicyScreen extends StatelessWidget { ('문의', '앱 배포 페이지에 표시된 개발자 연락처를 이용할 수 있습니다.'), ]; @override - Widget build(BuildContext context) => Scaffold( - body: RecurrenceSheet( - title: '개인정보 처리방침', - spacing: 8, - children: [ - const Padding( - padding: EdgeInsets.fromLTRB(4, 8, 4, 8), - child: Text('시행일: 2026년 8월 28일', style: TextStyle(fontSize: 12)), - ), - const Padding( - padding: EdgeInsets.fromLTRB(4, 0, 4, 16), - child: Text('OnTime 로컬 전용 개인정보 처리방침', style: TextStyle(fontSize: 14)), + Widget build(BuildContext context) => DataStatePage( + title: '개인정보 처리방침', + // Keep the iOS back target 44px tall; the seven-pixel header difference is + // removed from the introductory top padding to retain the reference grid. + padding: const EdgeInsets.fromLTRB(16, 11, 16, 24), + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '시행일: 2026년 8월 28일', + style: TextStyle( + fontSize: 12, + height: 17 / 12, + color: Color(0xFF545454), + ), + ), + SizedBox(height: 10), + Text( + 'OnTime 로컬 전용 개인정보 처리방침', + style: TextStyle(fontSize: 13, height: 18 / 13), + ), + SizedBox(height: 25), + ], ), - for (var i = 0; i < sections.length; i++) - RecurrencePanel( - child: SelectionArea( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 28, - child: Text( - '${i + 1}.', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.primary, - ), + ), + for (var i = 0; i < sections.length; i++) ...[ + Container( + key: Key('privacySection${i + 1}'), + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(10), + ), + child: SelectionArea( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 16, + child: Text( + '${i + 1}.', + style: const TextStyle( + fontSize: 14, + height: 20 / 14, + fontWeight: FontWeight.w700, + color: Color(0xFF4F69DF), ), ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - sections[i].$1, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + sections[i].$1, + style: const TextStyle( + fontSize: 13.5, + height: 20 / 13.5, + fontWeight: FontWeight.w700, ), - const SizedBox(height: 6), - Text( - sections[i].$2, - style: const TextStyle(fontSize: 13, height: 1.4), + ), + const SizedBox(height: 5), + Text( + sections[i].$2, + style: const TextStyle( + fontSize: 12.3, + height: 17 / 12.3, + color: Color(0xFF383838), ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), + ), + const SizedBox(height: 6), ], - ), + ], ); } diff --git a/lib/presentation/notification_allow/screens/notification_allow_screen.dart b/lib/presentation/notification_allow/screens/notification_allow_screen.dart index a591e4f3..bf84e0a7 100644 --- a/lib/presentation/notification_allow/screens/notification_allow_screen.dart +++ b/lib/presentation/notification_allow/screens/notification_allow_screen.dart @@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/services/notification_service.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/app/cubit/notification_gate_cubit.dart'; -import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; +import 'package:on_time_front/presentation/recurring/recurrence_components.dart'; abstract interface class NotificationPermissionGateway { Future checkNotificationPermission(); @@ -82,28 +82,32 @@ class _NotificationAllowScreenState extends State @override Widget build(BuildContext context) { - return Scaffold( - body: Padding( - padding: const EdgeInsets.only(bottom: 72.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 68.50, - children: [ - Expanded( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 40, - children: [_Image(), _Title()], + return RefreshTheme( + child: Scaffold( + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + 16, + (MediaQuery.sizeOf(context).height * .244 - + MediaQuery.paddingOf(context).top) + .clamp(24, 206), + 16, + 24, + ), + child: const Column( + children: [_Image(), SizedBox(height: 40), _Title()], + ), ), ), - ), - _Buttons(permissionGateway: widget.permissionGateway), - ], + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: _Buttons(permissionGateway: widget.permissionGateway), + ), + ], + ), ), ), ); @@ -123,22 +127,36 @@ class _Buttons extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, - spacing: 24, + spacing: 16, children: [ - FilledButton( - onPressed: () async { - await _handleNotificationPermission(context, permissionGateway); - }, - child: Text( - AppLocalizations.of(context)!.allowNotifications, - textAlign: TextAlign.center, - style: textTheme.titleMedium?.copyWith( - color: colorScheme.onPrimary, + SizedBox( + width: double.infinity, + height: 58, + child: FilledButton( + style: FilledButton.styleFrom( + backgroundColor: colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () async { + await _handleNotificationPermission(context, permissionGateway); + }, + child: Text( + AppLocalizations.of(context)!.allowNotifications, + textAlign: TextAlign.center, + style: textTheme.titleMedium?.copyWith( + color: colorScheme.onPrimary, + ), ), ), ), - GestureDetector( - onTap: () async { + TextButton( + style: TextButton.styleFrom( + minimumSize: const Size(double.infinity, 44), + foregroundColor: const Color(0xFF545454), + ), + onPressed: () async { await context.read().dismissPrompt(); if (!context.mounted) return; context.go('/home'); @@ -149,9 +167,7 @@ class _Buttons extends StatelessWidget { AppLocalizations.of(context)!.doItLater, textAlign: TextAlign.center, style: textTheme.bodyLarge?.copyWith( - color: AppColors.grey[400], - decoration: TextDecoration.underline, - decorationColor: AppColors.grey[400], + color: const Color(0xFF545454), ), ), ), @@ -201,15 +217,16 @@ class _Image extends StatelessWidget { return Container( width: 70, height: 70, - padding: const EdgeInsets.all(17.50), + alignment: Alignment.center, decoration: ShapeDecoration( color: colorScheme.primaryContainer, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(35)), ), child: SvgPicture.asset( - 'bell-ringing.svg', - package: 'assets', - colorFilter: ColorFilter.mode(colorScheme.primary, BlendMode.srcIn), + 'assets/design/notification_bell.svg', + width: 36, + height: 37, + excludeFromSemantics: true, ), ); } diff --git a/lib/presentation/onboarding/components/onboarding_page_view_layout.dart b/lib/presentation/onboarding/components/onboarding_page_view_layout.dart index 74f5b294..5dab0228 100644 --- a/lib/presentation/onboarding/components/onboarding_page_view_layout.dart +++ b/lib/presentation/onboarding/components/onboarding_page_view_layout.dart @@ -8,12 +8,14 @@ class OnboardingPageViewLayout extends StatefulWidget { this.subTitle, this.hint, required this.child, + this.contentSpacing = 18, }); final String title; final String? hint; final RichText? subTitle; final Widget child; + final double contentSpacing; @override State createState() => @@ -23,24 +25,31 @@ class OnboardingPageViewLayout extends StatefulWidget { class _OnboardingPageViewLayoutState extends State { @override Widget build(BuildContext context) { - return Column( - children: [ - SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 27.0, - horizontal: 8.0, - ), - child: OnboardingTitle( - title: widget.title, - subTitle: widget.subTitle, - hint: widget.hint, + return LayoutBuilder( + builder: (context, constraints) => Column( + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxHeight: constraints.maxHeight * .6), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only( + top: 40, + bottom: widget.contentSpacing, + ), + child: SizedBox( + width: double.infinity, + child: OnboardingTitle( + title: widget.title, + subTitle: widget.subTitle, + hint: widget.hint, + ), + ), + ), ), ), - ), - Expanded(child: widget.child), - ], + Expanded(child: widget.child), + ], + ), ); } } diff --git a/lib/presentation/onboarding/components/onboarding_title.dart b/lib/presentation/onboarding/components/onboarding_title.dart index f9976d79..b21357b9 100644 --- a/lib/presentation/onboarding/components/onboarding_title.dart +++ b/lib/presentation/onboarding/components/onboarding_title.dart @@ -20,9 +20,14 @@ class OnboardingTitle extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ RichText( + textScaler: MediaQuery.textScalerOf(context), text: TextSpan( text: title, - style: textTheme.titleLarge, + style: textTheme.titleLarge?.copyWith( + fontSize: 24, + height: 1.4, + fontWeight: FontWeight.w500, + ), children: hint != null ? [ TextSpan( @@ -35,8 +40,10 @@ class OnboardingTitle extends StatelessWidget { : [], ), ), - SizedBox(height: 8.0), - subTitle ?? const SizedBox.shrink(), + if (subTitle != null) ...[ + const SizedBox(height: 7), + SizedBox(width: double.infinity, child: subTitle!), + ], ], ); } diff --git a/lib/presentation/onboarding/preparation_name_select/components/create_icon_button.dart b/lib/presentation/onboarding/preparation_name_select/components/create_icon_button.dart index 324ab724..b788cdb4 100644 --- a/lib/presentation/onboarding/preparation_name_select/components/create_icon_button.dart +++ b/lib/presentation/onboarding/preparation_name_select/components/create_icon_button.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:on_time_front/l10n/app_localizations.dart'; +import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; class CreateIconButton extends StatelessWidget { const CreateIconButton({super.key, required this.onCreationRequested}); @@ -7,20 +9,20 @@ class CreateIconButton extends StatelessWidget { @override Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; return IconButton( - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - colorScheme.surfaceContainerHigh, + tooltip: AppLocalizations.of(context)?.addPreparationStep, + constraints: const BoxConstraints.tightFor(width: 44, height: 44), + padding: EdgeInsets.zero, + onPressed: onCreationRequested, + icon: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: AppColors.grey[250], + borderRadius: BorderRadius.circular(16), ), + child: Icon(Icons.add, size: 24, color: AppColors.grey.shade700), ), - onPressed: () { - onCreationRequested(); - }, - color: colorScheme.onPrimary, - icon: Icon(Icons.add), - padding: EdgeInsets.zero, - iconSize: 30.0, ); } } diff --git a/lib/presentation/onboarding/preparation_name_select/components/preparation_name_select_field.dart b/lib/presentation/onboarding/preparation_name_select/components/preparation_name_select_field.dart index e6bc7287..84ec85a3 100644 --- a/lib/presentation/onboarding/preparation_name_select/components/preparation_name_select_field.dart +++ b/lib/presentation/onboarding/preparation_name_select/components/preparation_name_select_field.dart @@ -55,7 +55,11 @@ class _PreparationNameSelectFieldState padding: const EdgeInsets.only(bottom: 8.0), child: Tile( key: ValueKey(widget.preparationStep.preparationId), - style: TileStyle(padding: EdgeInsets.all(16.0)), + style: TileStyle( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + borderRadius: BorderRadius.circular(9999), + backgroundColor: const Color(0xFFF6F6F6), + ), leading: SizedBox( width: 30, height: 30, @@ -66,7 +70,7 @@ class _PreparationNameSelectFieldState ), child: Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Container( constraints: BoxConstraints(minHeight: 30), child: Center( @@ -83,7 +87,12 @@ class _PreparationNameSelectFieldState decoration: InputDecoration( isDense: true, border: InputBorder.none, - contentPadding: EdgeInsets.all(3.0), + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, ), style: textTheme.bodyLarge, focusNode: focusNode, diff --git a/lib/presentation/onboarding/preparation_name_select/cubit/preparation_name/preparation_name_state.dart b/lib/presentation/onboarding/preparation_name_select/cubit/preparation_name/preparation_name_state.dart index bb945529..d36a4a55 100644 --- a/lib/presentation/onboarding/preparation_name_select/cubit/preparation_name/preparation_name_state.dart +++ b/lib/presentation/onboarding/preparation_name_select/cubit/preparation_name/preparation_name_state.dart @@ -31,7 +31,7 @@ final class PreparationNameState extends Equatable { final onBoardingPreparationSuggestion = [ PreparationStepNameState( - preparationName: PreparationNameInputModel.dirty('화장실 가기'), + preparationName: PreparationNameInputModel.dirty('샤워하기'), isSelected: false, ), PreparationStepNameState( @@ -42,6 +42,10 @@ final onBoardingPreparationSuggestion = [ preparationName: PreparationNameInputModel.dirty('머리 세팅하기'), isSelected: false, ), + PreparationStepNameState( + preparationName: PreparationNameInputModel.dirty('옷 고르기/입기'), + isSelected: false, + ), PreparationStepNameState( preparationName: PreparationNameInputModel.dirty('짐 챙기기'), isSelected: false, diff --git a/lib/presentation/onboarding/preparation_order/components/reorderable_tile.dart b/lib/presentation/onboarding/preparation_order/components/reorderable_tile.dart index 1eda7b38..a60ddb87 100644 --- a/lib/presentation/onboarding/preparation_order/components/reorderable_tile.dart +++ b/lib/presentation/onboarding/preparation_order/components/reorderable_tile.dart @@ -11,13 +11,17 @@ class ReorderableTile extends StatelessWidget { required this.index, }); - final dragIndicatorSvg = SvgPicture.asset( - 'drag_indicator.svg', - package: 'assets', - semanticsLabel: 'drag indicator', - height: 14, - width: 14, - fit: BoxFit.contain, + final dragIndicatorSvg = SizedBox( + height: 24, + width: 24, + child: Center( + child: SvgPicture.asset( + 'assets/design/onboarding_drag.svg', + semanticsLabel: 'drag indicator', + height: 16, + width: 18, + ), + ), ); final PreparationStepOrderState preparationStepOrderState; final int index; @@ -28,8 +32,10 @@ class ReorderableTile extends StatelessWidget { final textTheme = Theme.of(context).textTheme; return Tile( style: TileStyle( - backgroundColor: Color(0xFFE6E9F9), - padding: EdgeInsets.all(18.0) + EdgeInsets.only(right: 15), + backgroundColor: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(9999), + minimumSize: const Size(0, 62), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), ), leading: Container( height: 22, @@ -43,8 +49,8 @@ class ReorderableTile extends StatelessWidget { (index + 1).toString(), style: TextStyle( color: colorScheme.surface, - fontSize: 10.48, - fontWeight: FontWeight.w600, + fontSize: 14, + fontWeight: FontWeight.w400, height: 1.4, ), ), @@ -54,12 +60,12 @@ class ReorderableTile extends StatelessWidget { index: index, child: dragIndicatorSvg, ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Text( - preparationStepOrderState.preparationName, - style: textTheme.bodyLarge?.copyWith( - color: colorScheme.onPrimaryContainer, + child: Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + preparationStepOrderState.preparationName, + style: textTheme.bodyLarge?.copyWith(color: colorScheme.onSurface), ), ), ), diff --git a/lib/presentation/onboarding/preparation_order/screens/preparation_order_form.dart b/lib/presentation/onboarding/preparation_order/screens/preparation_order_form.dart index d0dcf513..1b951720 100644 --- a/lib/presentation/onboarding/preparation_order/screens/preparation_order_form.dart +++ b/lib/presentation/onboarding/preparation_order/screens/preparation_order_form.dart @@ -22,6 +22,7 @@ class _PreparationOrderFormState extends State { @override Widget build(BuildContext context) { return OnboardingPageViewLayout( + contentSpacing: 40, title: AppLocalizations.of(context)!.preparationOrderTitle, child: BlocBuilder( builder: (context, state) { diff --git a/lib/presentation/onboarding/preparation_time/components/preparation_time_input_list.dart b/lib/presentation/onboarding/preparation_time/components/preparation_time_input_list.dart index d8bc86c6..d3158bdc 100644 --- a/lib/presentation/onboarding/preparation_time/components/preparation_time_input_list.dart +++ b/lib/presentation/onboarding/preparation_time/components/preparation_time_input_list.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:on_time_front/presentation/onboarding/preparation_time/components/preparation_time_tile.dart'; import 'package:on_time_front/presentation/onboarding/preparation_time/cubit/preparation_time_cubit.dart'; @@ -23,7 +22,9 @@ class _PreparationTimeInputFieldListState @override Widget build(BuildContext context) { return SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 16), child: ListView.builder( + padding: EdgeInsets.zero, physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: widget.preparationTimeList.length, diff --git a/lib/presentation/onboarding/preparation_time/components/preparation_time_tile.dart b/lib/presentation/onboarding/preparation_time/components/preparation_time_tile.dart index df52d355..4453564b 100644 --- a/lib/presentation/onboarding/preparation_time/components/preparation_time_tile.dart +++ b/lib/presentation/onboarding/preparation_time/components/preparation_time_tile.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/onboarding/preparation_time/cubit/preparation_time_cubit.dart'; -import 'package:on_time_front/presentation/shared/components/cupertino_picker_modal.dart'; -import 'package:on_time_front/presentation/shared/components/tile.dart'; -import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; -import 'package:on_time_front/presentation/shared/theme/tile_style.dart'; -class PreparationTimeTile extends StatelessWidget { +class PreparationTimeTile extends StatefulWidget { const PreparationTimeTile({ super.key, required this.value, @@ -15,66 +13,122 @@ class PreparationTimeTile extends StatelessWidget { final PreparationStepTimeState value; final int index; + final void Function(int index, Duration value) onPreparationTimeChanged; - final Function(int index, Duration value) onPreparationTimeChanged; + @override + State createState() => _PreparationTimeTileState(); +} + +class _PreparationTimeTileState extends State { + late final TextEditingController _controller = TextEditingController( + text: widget.value.preparationTime.value.inMinutes.toString().padLeft( + 2, + '0', + ), + ); + final _focusNode = FocusNode(); + + void _focusAndSelect() { + _focusNode.requestFocus(); + _controller.selection = TextSelection( + baseOffset: 0, + extentOffset: _controller.text.length, + ); + } + + @override + void didUpdateWidget(covariant PreparationTimeTile oldWidget) { + super.didUpdateWidget(oldWidget); + if (!_focusNode.hasFocus && + oldWidget.value.preparationTime.value != + widget.value.preparationTime.value) { + _controller.text = widget.value.preparationTime.value.inMinutes + .toString() + .padLeft(2, '0'); + } + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - return Tile( - style: TileStyle( - margin: EdgeInsets.only(bottom: 8), - backgroundColor: Color(0xFFE6E9F9), - padding: EdgeInsets.only(left: 36, right: 40, top: 18, bottom: 18), - ), - trailing: Row( - children: [ - GestureDetector( - child: Container( - decoration: BoxDecoration( - color: AppColors.blue.shade300, - borderRadius: BorderRadius.circular(4), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 1.0, - ), + final colors = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _focusAndSelect, + child: Container( + constraints: const BoxConstraints(minHeight: 62), + decoration: BoxDecoration( + color: colors.surfaceContainerLow, + borderRadius: BorderRadius.circular(9999), + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 9), + child: Row( + children: [ + Expanded( child: Text( - (value.preparationTime.value.inMinutes < 10 ? '0' : '') + - (value.preparationTime.value.inMinutes < 0 - ? '0' - : value.preparationTime.value.inMinutes.toString()), - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onPrimaryContainer, + widget.value.preparationName, + style: const TextStyle(fontSize: 16, height: 1.4), + ), + ), + const SizedBox(width: 12), + Semantics( + label: + '${widget.value.preparationName} ${l10n.preparationTime} (${l10n.minutes})', + child: SizedBox( + width: Localizations.localeOf(context).languageCode == 'ko' + ? (_controller.text.length > 4 ? 108 : 68) + : (_controller.text.length > 4 ? 152 : 112), + child: TextFormField( + key: ValueKey('onboardingMinutes${widget.index}'), + controller: _controller, + focusNode: _focusNode, + keyboardType: TextInputType.number, + textInputAction: TextInputAction.done, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(6), + ], + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16, height: 1.4), + decoration: InputDecoration( + filled: true, + fillColor: colors.primaryContainer, + suffixText: l10n.minutes, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 10, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: BorderSide.none, + ), + ), + onTap: _focusAndSelect, + onChanged: (text) => widget.onPreparationTimeChanged( + widget.index, + Duration(minutes: int.tryParse(text) ?? 0), + ), + onFieldSubmitted: (_) => _focusNode.unfocus(), + onTapOutside: (_) => _focusNode.unfocus(), ), ), ), - ), - onTap: () { - context.showCupertinoMinutePickerModal( - title: '시간을 선택해주세요', - initialValue: value.preparationTime.value, - onSaved: (value) { - onPreparationTimeChanged(index, value); - }, - ); - }, + ], ), - SizedBox(width: 10), - Text( - '분', - style: textTheme.bodyLarge?.copyWith( - color: colorScheme.onPrimaryContainer, - ), - ), - ], - ), - child: Text( - value.preparationName, - style: textTheme.bodyLarge?.copyWith( - color: colorScheme.onPrimaryContainer, ), ), ); diff --git a/lib/presentation/onboarding/preparation_time/screens/preparation_time_form.dart b/lib/presentation/onboarding/preparation_time/screens/preparation_time_form.dart index 6cce3e0b..ea0b7f83 100644 --- a/lib/presentation/onboarding/preparation_time/screens/preparation_time_form.dart +++ b/lib/presentation/onboarding/preparation_time/screens/preparation_time_form.dart @@ -21,21 +21,36 @@ class _PreparationTimeFormState extends State { @override Widget build(BuildContext context) { - return OnboardingPageViewLayout( - title: AppLocalizations.of(context)!.preparationTimeTitle, - child: BlocBuilder( - builder: (context, state) { - return PreparationTimeInputFieldList( + return BlocBuilder( + builder: (context, state) { + final total = state.preparationTimeList.fold( + 0, + (sum, step) => sum + step.preparationTime.value.inMinutes, + ); + return OnboardingPageViewLayout( + title: AppLocalizations.of(context)!.preparationTimeTitle, + contentSpacing: 12, + subTitle: RichText( + textAlign: TextAlign.right, + textScaler: MediaQuery.textScalerOf(context), + text: TextSpan( + text: + '${AppLocalizations.of(context)!.totalTime}$total${Localizations.localeOf(context).languageCode == 'ko' ? '분' : ' min'}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + height: 1.4, + color: Color(0xFF545454), + ), + ), + ), + child: PreparationTimeInputFieldList( preparationTimeList: state.preparationTimeList, - onPreparationTimeChanged: (index, value) { - context.read().preparationTimeChanged( - index, - value, - ); - }, - ); - }, - ), + onPreparationTimeChanged: (index, value) => context + .read() + .preparationTimeChanged(index, value), + ), + ); + }, ); } } diff --git a/lib/presentation/onboarding/screens/onboarding_screen.dart b/lib/presentation/onboarding/screens/onboarding_screen.dart index 2d95b782..183838ce 100644 --- a/lib/presentation/onboarding/screens/onboarding_screen.dart +++ b/lib/presentation/onboarding/screens/onboarding_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/core/di/di_setup.dart'; +import 'package:on_time_front/presentation/recurring/recurrence_components.dart'; import 'package:on_time_front/presentation/onboarding/preparation_order/screens/preparation_order_form.dart'; import 'package:on_time_front/presentation/onboarding/preparation_name_select/cubit/preparation_name/preparation_name_cubit.dart'; import 'package:on_time_front/presentation/onboarding/preparation_name_select/screens/preparation_name_form.dart'; @@ -13,7 +14,6 @@ import 'package:on_time_front/presentation/onboarding/schedule_spare_time/screen import 'package:on_time_front/presentation/onboarding/cubit/onboarding_cubit.dart'; import 'package:on_time_front/presentation/onboarding/preparation_order/cubit/preparation_order_cubit.dart'; import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart'; -import 'package:on_time_front/presentation/shared/components/step_progress.dart'; import 'package:on_time_front/presentation/shared/components/two_action_dialog.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; @@ -24,7 +24,12 @@ class OnboardingScreen extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => getIt.get(), - child: Scaffold(resizeToAvoidBottomInset: false, body: _OnboardingForm()), + child: const RefreshTheme( + child: Scaffold( + resizeToAvoidBottomInset: false, + body: _OnboardingForm(), + ), + ), ); } } @@ -66,7 +71,12 @@ class _OnboardingFormState extends State<_OnboardingForm> Widget build(BuildContext context) { return SafeArea( child: Padding( - padding: const EdgeInsets.all(16.0), + padding: EdgeInsets.fromLTRB( + 16, + 12, + 16, + 13 + MediaQuery.viewInsetsOf(context).bottom, + ), child: MultiBlocProvider( providers: [ BlocProvider( @@ -111,25 +121,34 @@ class _OnboardingFormState extends State<_OnboardingForm> ], ), ), - SizedBox( - height: 58, - width: double.infinity, - child: ElevatedButton( - onPressed: - !_isSubmitting && - context.select( - (OnboardingCubit cubit) => cubit.state.isValid, + if (MediaQuery.viewInsetsOf(context).bottom == 0) + SizedBox( + height: 58, + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.primary, + ), + onPressed: + !_isSubmitting && + context.select( + (OnboardingCubit cubit) => + cubit.state.isValid, + ) + ? () => _onNextPageButtonClicked(context) + : null, + child: _isSubmitting + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), ) - ? () => _onNextPageButtonClicked(context) - : null, - child: _isSubmitting - ? const SizedBox.square( - dimension: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : Text(AppLocalizations.of(context)!.next), + : Text(AppLocalizations.of(context)!.next), + ), ), - ), ], ); }, @@ -207,7 +226,7 @@ class _OnboardingFormState extends State<_OnboardingForm> } class _AppBar extends StatelessWidget { - _AppBar({ + const _AppBar({ required this.tabController, required this.onUpdateCurrentPageIndex, }); @@ -215,43 +234,96 @@ class _AppBar extends StatelessWidget { final TabController tabController; final void Function(int) onUpdateCurrentPageIndex; - final SvgPicture _previousIcon = SvgPicture.asset( - 'chevron_left.svg', - package: 'assets', - semanticsLabel: 'Previous Icon', - fit: BoxFit.contain, - ); - @override - Widget build(BuildContext context) { - const double iconButtonSize = 32.0; - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: iconButtonSize, - height: iconButtonSize, - child: IconButton( - padding: EdgeInsets.zero, - onPressed: () { - onUpdateCurrentPageIndex(tabController.index - 1); - }, - icon: _previousIcon, + Widget build(BuildContext context) => SizedBox( + height: 39, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: -8, + top: -4, + child: SizedBox( + width: 44, + height: 44, + child: IconButton( + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + onPressed: () => + onUpdateCurrentPageIndex(tabController.index - 1), + icon: SvgPicture.asset( + 'chevron_left.svg', + package: 'assets', + width: 8, + height: 14, + colorFilter: const ColorFilter.mode( + Colors.black, + BlendMode.srcIn, + ), + ), + ), ), ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StepProgress( - currentStep: tabController.index, - totalSteps: tabController.length, + Positioned.fill( + left: 20, + child: Center( + child: Semantics( + label: 'STEP ${tabController.index + 1} / 4', + child: ExcludeSemantics( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < 3; i++) ...[ + SvgPicture.asset( + 'assets/design/onboarding_progress_${i < tabController.index + ? 'complete' + : i == tabController.index + ? 'current' + : 'pending'}.svg', + width: 74, + height: 11, + ), + const SizedBox(width: 6), + ], + SvgPicture.asset( + 'assets/design/onboarding_progress_last_${tabController.index == 3 ? 'current' : 'pending'}.svg', + width: 11, + height: 11, + ), + ], + ), + const SizedBox(height: 9), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < 4; i++) ...[ + if (i > 0) const SizedBox(width: 38), + Text( + 'STEP ${i + 1}', + textScaler: TextScaler.noScaling, + style: TextStyle( + fontSize: 12, + height: 1.4, + color: i <= tabController.index + ? const Color(0xFF4F69DF) + : const Color(0xFF949494), + ), + ), + ], + ], + ), + ], + ), + ), + ), ), ), ), - SizedBox(width: iconButtonSize), ], - ); - } + ), + ); } diff --git a/lib/presentation/onboarding/screens/onboarding_start_screen.dart b/lib/presentation/onboarding/screens/onboarding_start_screen.dart index 5e442409..69d5a1bd 100644 --- a/lib/presentation/onboarding/screens/onboarding_start_screen.dart +++ b/lib/presentation/onboarding/screens/onboarding_start_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:on_time_front/presentation/shared/theme/theme.dart'; +import 'package:on_time_front/presentation/recurring/recurrence_components.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; class OnboardingStartScreen extends StatelessWidget { @@ -9,28 +8,35 @@ class OnboardingStartScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 35), - child: Center( - child: Column( - children: [ - Expanded( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _Title(), - SizedBox(height: 37), - _OnboardingCharacterImage(), - ], - ), + return RefreshTheme( + child: Scaffold( + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + 16, + (MediaQuery.sizeOf(context).height * .174 - + MediaQuery.paddingOf(context).top) + .clamp(24, 147), + 16, + 24, + ), + child: const Column( + children: [ + _Title(), + SizedBox(height: 46), + _OnboardingCharacterImage(), + ], ), ), - _OnboardingStartButton(), - ], - ), + ), + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 13), + child: _OnboardingStartButton(), + ), + ], ), ), ), @@ -57,7 +63,7 @@ class _Title extends StatelessWidget { AppLocalizations.of(context)!.onboardingStartSubtitle, textAlign: TextAlign.center, key: Key('onboarding_start_subtitle'), - style: textTheme.titleExtraSmall, + style: textTheme.bodyLarge?.copyWith(color: const Color(0xFF777777)), ), ], ); @@ -69,12 +75,11 @@ class _OnboardingCharacterImage extends StatelessWidget { @override Widget build(BuildContext context) { - return SvgPicture.asset( - 'characters/onboarding_character.svg', - package: 'assets', - semanticsLabel: 'character onboarding', - height: 271, - width: 280, + return Image.asset( + 'assets/design/onboarding_greeting.png', + excludeFromSemantics: true, + height: 280, + width: 271, fit: BoxFit.contain, ); } @@ -87,7 +92,11 @@ class _OnboardingStartButton extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( width: double.infinity, + height: 58, child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + ), onPressed: () { context.go('/onboarding'); }, diff --git a/lib/presentation/recurring/recurrence_labels.dart b/lib/presentation/recurring/recurrence_labels.dart index e3eb2593..a37b65d6 100644 --- a/lib/presentation/recurring/recurrence_labels.dart +++ b/lib/presentation/recurring/recurrence_labels.dart @@ -45,3 +45,29 @@ String recurrenceEndLabel(BuildContext context, RecurrenceRule rule) => : rule.until != null ? '${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(rule.until!)}${recurrenceText(context, '까지', ' inclusive')}' : recurrenceText(context, '종료 없음', 'No end date'); + +/// A compact recurrence label for the date/time and review cards. +String recurrencePatternLabel( + BuildContext context, + RecurrenceRule rule, { + bool includeTime = false, +}) { + if (Localizations.localeOf(context).languageCode != 'ko') { + return recurrenceLabel(context, rule); + } + final pattern = switch (rule.frequency) { + RecurrenceFrequency.daily => + rule.interval == 1 ? '매일' : '${rule.interval}일마다', + RecurrenceFrequency.weekly => + '${rule.interval == 1 ? '매주' : '${rule.interval}주마다'} ${(rule.weekdays.toList()..sort()).map((d) => weekdayLabel(context, d)).join(' · ')}', + RecurrenceFrequency.monthly => + '${rule.interval == 1 ? '매월' : '${rule.interval}개월마다'} ${switch (rule.monthly) { + MonthlyRecurrence.dayOfMonth => '${rule.monthDay}일', + MonthlyRecurrence.lastDay => '마지막 날', + MonthlyRecurrence.nthWeekday => '${rule.ordinal == -1 ? '마지막' : '${rule.ordinal}번째'} ${weekdayLabel(context, rule.monthWeekday)}요일', + }}', + }; + return includeTime + ? '$pattern ${DateFormat.jm('ko').format(rule.start)}' + : pattern; +} diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart index 0953229e..5c9a84e0 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_create_list.dart @@ -53,11 +53,11 @@ class PreparationFormCreateList extends StatelessWidget { ), ), ), - SizedBox(height: 28.0), + const SizedBox(height: 8), Center( child: SizedBox( - height: 30, - width: 30, + height: 44, + width: 44, child: CreateIconButton(onCreationRequested: onCreationRequested), ), ), diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_list_field.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_list_field.dart index bee5dd03..990cd795 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_list_field.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_form_list_field.dart @@ -9,6 +9,7 @@ import 'package:on_time_front/presentation/onboarding/preparation_time/input_mod import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_time_input.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/cubit/preparation_step_form_cubit.dart'; import 'package:on_time_front/presentation/shared/components/tile.dart'; +import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; import 'package:on_time_front/presentation/shared/theme/tile_style.dart'; class PreparationFormListField extends StatefulWidget { @@ -58,10 +59,10 @@ class _PreparationFormListFieldState extends State { bool _hasRequestedInitialFocus = false; bool _isTimePickerOpen = false; final dragIndicatorSvg = SvgPicture.asset( - 'drag_indicator.svg', + 'preparation_edit_drag.svg', package: 'assets', semanticsLabel: 'drag indicator', - height: 18, + height: 16, width: 18, fit: BoxFit.contain, ); @@ -206,7 +207,10 @@ class _PreparationFormListFieldState extends State { return TextFieldTapRegion( child: Tile( key: ValueKey(widget.preparationStep.id), - style: TileStyle(padding: EdgeInsets.fromLTRB(21, 19, 21, 19)), + style: TileStyle( + backgroundColor: AppColors.grey.shade50, + padding: const EdgeInsets.fromLTRB(21, 16, 30, 16), + ), leading: widget.index == null ? dragIndicatorSvg : ReorderableDragStartListener( diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_time_input.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_time_input.dart index 56137d66..c97d2015 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_time_input.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/components/preparation_time_input.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:on_time_front/l10n/app_localizations.dart'; import 'package:on_time_front/presentation/shared/components/cupertino_picker_modal.dart'; import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; @@ -22,12 +23,15 @@ class PreparationTimeInput extends StatelessWidget { Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final colorScheme = Theme.of(context).colorScheme; + final minutes = time.inMinutes < 0 ? 0 : time.inMinutes; + final scale = MediaQuery.textScalerOf(context).scale(1); + final l10n = AppLocalizations.of(context)!; return Row( children: [ GestureDetector( child: Container( - width: 50, - height: 30, + width: 50 * scale, + height: 30 * scale, decoration: BoxDecoration( color: AppColors.white, border: hasError @@ -37,10 +41,9 @@ class PreparationTimeInput extends StatelessWidget { ), child: Center( child: Text( - (time.inMinutes < 10 ? '0' : '') + - (time.inMinutes < 0 ? '0' : time.inMinutes.toString()), - style: textTheme.titleSmall?.copyWith( - color: colorScheme.onPrimaryContainer, + '${minutes.toString().padLeft(2, '0')} ${l10n.localeName.startsWith('ko') ? '분' : 'min'}', + style: textTheme.bodyLarge?.copyWith( + color: colorScheme.onSurface, ), ), ), @@ -48,7 +51,7 @@ class PreparationTimeInput extends StatelessWidget { onTap: () { onTap?.call(); context.showCupertinoMinutePickerModal( - title: '시간을 선택해주세요', + title: l10n.selectTime, initialValue: time, onSaved: (value) { onPreparationTimeChanged?.call(value); diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/screens/preparation_edit_form.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/screens/preparation_edit_form.dart index 0d8844cc..bb5ffab9 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/screens/preparation_edit_form.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/screens/preparation_edit_form.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:on_time_front/l10n/app_localizations.dart'; +import 'package:on_time_front/presentation/shared/utils/duration_format.dart'; import 'package:on_time_front/core/di/di_setup.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; import 'package:on_time_front/presentation/schedule_create/schedule_spare_and_preparing_time/preparation_form/bloc/preparation_form_bloc.dart'; @@ -38,6 +40,8 @@ class _PreparationEditFormState extends State { return Column( children: [ TopBar( + title: AppLocalizations.of(context)!.editPreparationTime, + actionLabel: AppLocalizations.of(context)!.done, onNextPageButtonClicked: state.isValid ? () { getIt.get().setDraft( @@ -49,24 +53,37 @@ class _PreparationEditFormState extends State { onPreviousPageButtonClicked: context.pop, isNextButtonEnabled: state.isValid, ), + const SizedBox(height: 31), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Align( + alignment: Alignment.centerRight, + child: Text( + '${AppLocalizations.of(context)!.totalTime}${formatDurationAsMinutes(context, Duration(minutes: state.preparationStepList.fold(0, (sum, step) => sum + step.preparationTime.value.inMinutes)))}', + ), + ), + ), const SizedBox(height: 24), Expanded( - child: PreparationFormCreateList( - preparationNameState: state, - onNameChanged: - ({required int index, required String value}) { - context.read().add( - PreparationFormPreparationStepNameChanged( - index: index, - preparationStepName: value, - ), - ); - }, - onCreationRequested: () { - context.read().add( - PreparationFormPreparationStepCreationRequested(), - ); - }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: PreparationFormCreateList( + preparationNameState: state, + onNameChanged: + ({required int index, required String value}) { + context.read().add( + PreparationFormPreparationStepNameChanged( + index: index, + preparationStepName: value, + ), + ); + }, + onCreationRequested: () { + context.read().add( + PreparationFormPreparationStepCreationRequested(), + ); + }, + ), ), ), ], diff --git a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart index 1b215116..96ff190b 100644 --- a/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart +++ b/lib/presentation/schedule_create/schedule_spare_and_preparing_time/screens/schedule_spare_and_preparing_time_form.dart @@ -2,6 +2,7 @@ import 'package:on_time_front/presentation/recurring/recurrence_components.dart' import 'package:on_time_front/domain/entities/preparation_step_entity.dart'; import 'package:on_time_front/presentation/recurring/recurrence_labels.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:on_time_front/domain/entities/preparation_entity.dart'; @@ -185,7 +186,8 @@ class _ScheduleSpareAndPreparingTimeFormState 'Review your preparation\nand edit as needed.', ), style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontSize: 23, + fontSize: 21, + height: 30 / 21, fontWeight: FontWeight.w700, ), ), @@ -196,7 +198,11 @@ class _ScheduleSpareAndPreparingTimeFormState '이 약속의 반복에만 적용되는 준비 과정입니다.\n기본 준비과정의 복사본으로 생성되어\n이곳에서 수정해도 기존 설정에는 영향을 주지 않습니다.', 'This preparation belongs only to this series. Changes do not affect your default preparation.', ), - style: Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.6), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 13.5, + height: 21 / 13.5, + color: const Color(0xff545454), + ), ), const SizedBox(height: 20), ReorderableListView.builder( @@ -229,13 +235,61 @@ class _ScheduleSpareAndPreparingTimeFormState child: RecurrencePanel( padding: EdgeInsets.zero, child: ListTile( + minTileHeight: 63, + horizontalTitleGap: 24, leading: ReorderableDragStartListener( index: i, - child: const Icon(Icons.drag_handle), + child: SvgPicture.asset( + 'recurrence_preparation_drag.svg', + package: 'assets', + ), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + steps[i].preparationName, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 7), + ClipRect( + child: SizedBox( + width: double.infinity, + child: Align( + alignment: Alignment.centerLeft, + child: SvgPicture.asset( + 'recurrence_preparation_underline.svg', + package: 'assets', + ), + ), + ), + ), + ], ), - title: Text(steps[i].preparationName), - trailing: Text( - formatDurationAsMinutes(context, steps[i].preparationTime), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + child: Text( + formatDurationAsMinutes( + context, + steps[i].preparationTime, + ), + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 12), + SvgPicture.asset( + 'recurrence_preparation_chevron.svg', + package: 'assets', + ), + ], ), onTap: () => _editPreparation(state), ), @@ -248,23 +302,54 @@ class _ScheduleSpareAndPreparingTimeFormState child: Text(recurrenceText(context, '준비 과정 추가', 'Add preparation')), ), const SizedBox(height: 4), - RecurrenceValue( - label: recurrenceText( - context, - '여유 (준비 합계 제외)', - 'Buffer (excluded from preparation total)', - ), - value: formatDurationAsMinutes( - context, - state.spareTime.value ?? Duration.zero, - ), - icon: Icons.timer_outlined, - onTap: () => context.showCupertinoMinutePickerModal( - title: AppLocalizations.of(context)!.enterTime, - initialValue: state.spareTime.value ?? Duration.zero, - onSaved: (value) => context - .read() - .spareTimeChanged(value), + RecurrencePanel( + padding: EdgeInsets.zero, + child: ListTile( + minTileHeight: 63, + horizontalTitleGap: 24, + leading: SvgPicture.asset( + 'recurrence_preparation_drag.svg', + package: 'assets', + ), + title: Text( + recurrenceText( + context, + '여유 (준비 합계 제외)', + 'Buffer (excluded from preparation total)', + ), + style: const TextStyle(fontSize: 18), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + child: Text( + formatDurationAsMinutes( + context, + state.spareTime.value ?? Duration.zero, + ), + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(width: 12), + SvgPicture.asset( + 'recurrence_preparation_chevron.svg', + package: 'assets', + ), + ], + ), + onTap: () => context.showCupertinoMinutePickerModal( + title: AppLocalizations.of(context)!.enterTime, + initialValue: state.spareTime.value ?? Duration.zero, + onSaved: (value) => context + .read() + .spareTimeChanged(value), + ), ), ), const SizedBox(height: 16), diff --git a/lib/presentation/shared/components/app_spinner.dart b/lib/presentation/shared/components/app_spinner.dart new file mode 100644 index 00000000..08429631 --- /dev/null +++ b/lib/presentation/shared/components/app_spinner.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class AppSpinner extends StatefulWidget { + const AppSpinner({ + super.key, + this.assetPath = 'feedback_spinner.svg', + this.assetPackage = 'assets', + }); + + final String assetPath; + final String? assetPackage; + + @override + State createState() => _AppSpinnerState(); +} + +class _AppSpinnerState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _rotation = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 900), + )..repeat(); + + @override + void dispose() { + _rotation.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Semantics( + label: '작업 중', + liveRegion: true, + child: RotationTransition( + turns: _rotation, + child: SvgPicture.asset( + widget.assetPath, + package: widget.assetPackage, + width: 40, + height: 40, + excludeFromSemantics: true, + ), + ), + ); +} diff --git a/lib/presentation/shared/components/cupertino_picker_modal.dart b/lib/presentation/shared/components/cupertino_picker_modal.dart index bbaa330a..b62ac2ec 100644 --- a/lib/presentation/shared/components/cupertino_picker_modal.dart +++ b/lib/presentation/shared/components/cupertino_picker_modal.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:on_time_front/l10n/app_localizations.dart'; +import 'package:on_time_front/presentation/shared/components/modal_wide_button.dart'; extension ModalBottomSheetExtension on BuildContext { void showCupertinoTimerPickerModal({ @@ -10,73 +11,18 @@ extension ModalBottomSheetExtension on BuildContext { required Function(Duration value) onSaved, VoidCallback? onDisposed, }) { - showModalBottomSheet( - isDismissible: true, - context: this, - builder: (BuildContext context) { - final textTheme = Theme.of(context).textTheme; - Duration duration = initialValue; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 29.0, vertical: 28.0), - child: SizedBox( - height: 334, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: textTheme.titleMedium), - Expanded( - child: Center( - child: CupertinoTimerPicker( - mode: mode, - initialTimerDuration: initialValue, - itemExtent: 32, - onTimerDurationChanged: (value) => duration = value, - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - flex: 1, - child: ElevatedButton( - onPressed: () { - //calling order matters - Navigator.pop(context); - }, - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - Color.fromARGB(255, 220, 227, 255), - ), - foregroundColor: WidgetStatePropertyAll( - Color.fromARGB(255, 92, 121, 251), - ), - ), - child: Text(AppLocalizations.of(this)!.cancel), - ), - ), - SizedBox(width: 20.0), - Expanded( - flex: 1, - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - onSaved(duration); - }, - child: Text(AppLocalizations.of(this)!.ok), - ), - ), - ], - ), - ], - ), - ), - ); - }, - ).whenComplete(() { - onDisposed?.call(); - }); + var duration = initialValue; + _showPicker( + title: title, + onSaved: () => onSaved(duration), + onDisposed: onDisposed, + picker: CupertinoTimerPicker( + mode: mode, + initialTimerDuration: initialValue, + itemExtent: 32, + onTimerDurationChanged: (value) => duration = value, + ), + ); } void showCupertinoMinutePickerModal({ @@ -85,93 +31,29 @@ extension ModalBottomSheetExtension on BuildContext { required Function(Duration value) onSaved, VoidCallback? onDisposed, }) { - showModalBottomSheet( - isDismissible: true, - context: this, - builder: (BuildContext context) { - final textTheme = Theme.of(context).textTheme; - int minutes = initialValue.inMinutes; - return ClipRRect( - borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)), - child: Container( - color: Theme.of(context).colorScheme.surface, - padding: const EdgeInsets.symmetric( - horizontal: 29.0, - vertical: 28.0, - ), - child: SizedBox( - height: 334, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: textTheme.titleMedium), - Expanded( - child: Center( - child: SizedBox( - width: 69.0, - child: CupertinoPicker( - scrollController: FixedExtentScrollController( - initialItem: initialValue.inMinutes, - ), - looping: true, - itemExtent: 32, - onSelectedItemChanged: (int value) { - minutes = value; - }, - children: List.generate( - 60, - (index) => Text( - (index < 10 ? '0' : '') + index.toString(), - ), - ), - ), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - flex: 1, - child: ElevatedButton( - onPressed: () { - //calling order matters - Navigator.pop(context); - }, - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - Color.fromARGB(255, 220, 227, 255), - ), - foregroundColor: WidgetStatePropertyAll( - Color.fromARGB(255, 92, 121, 251), - ), - ), - child: Text(AppLocalizations.of(this)!.cancel), - ), - ), - SizedBox(width: 20.0), - Expanded( - flex: 1, - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - onSaved(Duration(minutes: minutes)); - }, - child: Text(AppLocalizations.of(this)!.ok), - ), - ), - ], - ), - ], - ), - ), - ), - ); + var minutes = initialValue.inMinutes; + final controller = FixedExtentScrollController(initialItem: minutes); + _showPicker( + title: title, + onSaved: () => onSaved(Duration(minutes: minutes)), + onDisposed: () { + controller.dispose(); + onDisposed?.call(); }, - ).whenComplete(() { - onDisposed?.call(); - }); + picker: SizedBox( + width: 69, + child: CupertinoPicker( + scrollController: controller, + looping: true, + itemExtent: 32, + onSelectedItemChanged: (value) => minutes = value, + children: List.generate( + 60, + (index) => Text(index.toString().padLeft(2, '0')), + ), + ), + ), + ); } void showCupertinoDatePickerModal({ @@ -180,75 +62,109 @@ extension ModalBottomSheetExtension on BuildContext { required Function(DateTime value) onSaved, required CupertinoDatePickerMode mode, VoidCallback? onDisposed, + }) { + var dateTime = initialValue; + _showPicker( + title: title, + onSaved: () => onSaved(dateTime), + onDisposed: onDisposed, + picker: CupertinoDatePicker( + mode: mode, + initialDateTime: initialValue, + itemExtent: 32, + onDateTimeChanged: (value) => dateTime = value, + ), + ); + } + + void _showPicker({ + required String title, + required Widget picker, + required VoidCallback onSaved, + VoidCallback? onDisposed, }) { showModalBottomSheet( - isDismissible: true, context: this, - builder: (BuildContext context) { - final textTheme = Theme.of(context).textTheme; - DateTime dateTime = initialValue; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 29.0, vertical: 28.0), - child: SizedBox( - height: 334, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: textTheme.titleMedium), - Expanded( - child: Center( - child: CupertinoDatePicker( - mode: mode, - initialDateTime: initialValue, - itemExtent: 32, - onDateTimeChanged: (DateTime value) { - dateTime = value; - }, - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - flex: 1, - child: ElevatedButton( - onPressed: () { - //calling order matters - Navigator.pop(context); - }, - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - Color.fromARGB(255, 220, 227, 255), - ), - foregroundColor: WidgetStatePropertyAll( - Color.fromARGB(255, 92, 121, 251), - ), - ), - child: Text(AppLocalizations.of(this)!.cancel), - ), - ), - SizedBox(width: 20.0), - Expanded( - flex: 1, - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - onSaved(dateTime); - }, - child: Text(AppLocalizations.of(this)!.ok), - ), - ), - ], + isScrollControlled: true, + barrierColor: const Color(0x6b000000), + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) => _PickerSheet( + title: title, + picker: picker, + onSaved: () { + Navigator.pop(context); + onSaved(); + }, + ), + ).whenComplete(() => onDisposed?.call()); + } +} + +class _PickerSheet extends StatelessWidget { + const _PickerSheet({ + required this.title, + required this.picker, + required this.onSaved, + }); + final String title; + final Widget picker; + final VoidCallback onSaved; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + final bottom = MediaQuery.paddingOf(context).bottom; + return Theme( + data: theme.copyWith( + colorScheme: theme.colorScheme.copyWith( + primary: const Color(0xff4f69df), + primaryContainer: const Color(0xffdce3ff), + onPrimaryContainer: const Color(0xff23346b), + ), + ), + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.fromLTRB(16, 24, 16, bottom < 21 ? 21 : bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + fontSize: 20, + height: 1.4, + fontWeight: FontWeight.w600, ), - ], - ), + ), + const SizedBox(height: 24), + SizedBox(height: 167, child: Center(child: picker)), + const SizedBox(height: 24), + Row( + children: [ + ModalWideButton( + layout: ModalWideButtonLayout.flex, + text: l10n.cancel, + variant: ModalWideButtonVariant.subtle, + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 16), + ModalWideButton( + layout: ModalWideButtonLayout.flex, + text: l10n.localeName.startsWith('ko') ? '입력' : l10n.ok, + variant: ModalWideButtonVariant.primary, + onPressed: onSaved, + ), + ], + ), + ], ), - ); - }, - ).whenComplete(() { - onDisposed?.call(); - }); + ), + ), + ); } } diff --git a/lib/presentation/shared/components/custom_alert_dialog.dart b/lib/presentation/shared/components/custom_alert_dialog.dart index abed29bc..2bbd7fd0 100644 --- a/lib/presentation/shared/components/custom_alert_dialog.dart +++ b/lib/presentation/shared/components/custom_alert_dialog.dart @@ -30,6 +30,7 @@ class CustomAlertDialog extends StatelessWidget { this.contentActionsSpacing, this.titleTextAlign = TextAlign.start, this.contentTextAlign = TextAlign.start, + this.constraints, }); final Widget? title; @@ -78,6 +79,7 @@ class CustomAlertDialog extends StatelessWidget { final double? contentActionsSpacing; final TextAlign titleTextAlign; final TextAlign contentTextAlign; + final BoxConstraints? constraints; @override Widget build(BuildContext context) { @@ -207,6 +209,7 @@ class CustomAlertDialog extends StatelessWidget { clipBehavior: clipBehavior, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), alignment: alignment, + constraints: constraints, child: dialogChild, ); } diff --git a/lib/presentation/shared/components/modal_wide_button.dart b/lib/presentation/shared/components/modal_wide_button.dart index 562b3b1b..271ba66b 100644 --- a/lib/presentation/shared/components/modal_wide_button.dart +++ b/lib/presentation/shared/components/modal_wide_button.dart @@ -30,7 +30,6 @@ class ModalWideButton extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; - final (backgroundColor, foregroundColor) = switch (variant) { ModalWideButtonVariant.neutral => ( colorScheme.surfaceContainerLow, @@ -45,8 +44,8 @@ class ModalWideButton extends StatelessWidget { colorScheme.onPrimary, ), ModalWideButtonVariant.destructive => ( - colorScheme.error, - colorScheme.onError, + const Color(0xFFBF2E22), + Colors.white, ), }; diff --git a/lib/presentation/shared/components/time_stepper.dart b/lib/presentation/shared/components/time_stepper.dart index ca2382a7..5abe8186 100644 --- a/lib/presentation/shared/components/time_stepper.dart +++ b/lib/presentation/shared/components/time_stepper.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:on_time_front/presentation/shared/constants/app_colors.dart'; class TimeStepper extends StatelessWidget { const TimeStepper({ @@ -16,31 +17,51 @@ class TimeStepper extends StatelessWidget { @override Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final ColorScheme colorScheme = Theme.of(context).colorScheme; - final iconButtonStyle = ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Color(0xffe6e9f9)), - foregroundColor: WidgetStatePropertyAll(colorScheme.primary), - shape: WidgetStatePropertyAll( - CircleBorder(side: BorderSide(color: colorScheme.primary, width: 1.0)), + final decreaseEnabled = value > lowerBound; + final buttonStyle = ButtonStyle( + fixedSize: const WidgetStatePropertyAll(Size(46, 46)), + padding: const WidgetStatePropertyAll(EdgeInsets.zero), + backgroundColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.disabled) + ? AppColors.grey.shade200 + : const Color(0xffdce3ff), ), + foregroundColor: WidgetStateProperty.resolveWith( + (states) => states.contains(WidgetState.disabled) + ? AppColors.grey.shade400 + : const Color(0xff3d54bc), + ), + side: WidgetStateProperty.resolveWith( + (states) => BorderSide( + color: states.contains(WidgetState.disabled) + ? AppColors.grey.shade300 + : const Color(0xff3d54bc), + ), + ), + shape: const WidgetStatePropertyAll(CircleBorder()), ); return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ IconButton( - icon: const Icon(Icons.remove), - style: iconButtonStyle, - onPressed: value > lowerBound ? onSpareTimeDecreased : null, + icon: const Icon(Icons.remove, size: 24), + style: buttonStyle, + onPressed: decreaseEnabled ? onSpareTimeDecreased : null, ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 35.0), - child: Text('${value.inMinutes}분', style: textTheme.titleSmall), + const SizedBox(width: 34.5), + Text( + '${value.inMinutes}분', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontSize: 24.2, + fontWeight: FontWeight.w400, + height: 1.8, + ), ), + const SizedBox(width: 34.5), IconButton( - icon: const Icon(Icons.add), - style: iconButtonStyle, + icon: const Icon(Icons.add, size: 24), + style: buttonStyle, onPressed: onSpareTimeIncreased, ), ], diff --git a/lib/presentation/shared/components/two_action_dialog.dart b/lib/presentation/shared/components/two_action_dialog.dart index 9fd098fb..f04bf4f5 100644 --- a/lib/presentation/shared/components/two_action_dialog.dart +++ b/lib/presentation/shared/components/two_action_dialog.dart @@ -23,10 +23,14 @@ class TwoActionDialogConfig { required this.primaryAction, this.secondaryAction, this.barrierDismissible = true, + this.useSafeArea = true, this.maxWidth = TwoActionDialogTokens.maxDialogWidth, this.innerPadding = TwoActionDialogTokens.innerPadding, this.titleContentSpacing = TwoActionDialogTokens.titleContentSpacing, this.contentActionsSpacing = TwoActionDialogTokens.contentActionsSpacing, + this.barrierColor, + this.alignment = Alignment.center, + this.insetPadding, }); final String title; @@ -34,10 +38,14 @@ class TwoActionDialogConfig { final DialogActionConfig primaryAction; final DialogActionConfig? secondaryAction; final bool barrierDismissible; + final bool useSafeArea; final double maxWidth; final EdgeInsets innerPadding; final double titleContentSpacing; final double contentActionsSpacing; + final Color? barrierColor; + final AlignmentGeometry alignment; + final EdgeInsets? insetPadding; } abstract final class TwoActionDialogTokens { @@ -45,7 +53,7 @@ abstract final class TwoActionDialogTokens { static const EdgeInsets innerPadding = EdgeInsets.fromLTRB(16, 18, 16, 18); static const double titleContentSpacing = 8; static const double contentActionsSpacing = 16; - static const double actionButtonHeight = 43; + static const double actionButtonHeight = 44; static const double actionSpacing = 8; const TwoActionDialogTokens._(); @@ -59,6 +67,8 @@ Future showTwoActionDialog( final result = await showDialog( context: context, barrierDismissible: config.barrierDismissible, + useSafeArea: config.useSafeArea, + barrierColor: config.barrierColor, builder: (dialogContext) { return TwoActionDialog( config: config, @@ -98,15 +108,23 @@ class TwoActionDialog extends StatelessWidget { final screenWidth = MediaQuery.sizeOf(context).width; final dialogWidth = (screenWidth - 32).clamp(0.0, config.maxWidth); + final actionsWidth = (dialogWidth - config.innerPadding.horizontal).clamp( + 0.0, + dialogWidth, + ); - final titleText = Text( - config.title, - style: textTheme.titleMedium?.copyWith( - fontFamily: 'Pretendard', - fontWeight: FontWeight.w600, - fontSize: 18, - height: 1.4, - color: colorScheme.onSurface, + final titleText = SizedBox( + width: actionsWidth, + child: Text( + config.title, + style: textTheme.titleMedium?.copyWith( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w600, + fontSize: 18, + height: 1.4, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.center, ), ); @@ -114,14 +132,19 @@ class TwoActionDialog extends StatelessWidget { customContent ?? (config.description == null ? null - : Text( - config.description!, - style: textTheme.bodyMedium?.copyWith( - fontFamily: 'Pretendard', - fontWeight: FontWeight.w400, - fontSize: 14, - height: 1.4, - color: colorScheme.outline, + : SizedBox( + width: actionsWidth, + child: Text( + config.description!, + style: textTheme.bodyMedium?.copyWith( + fontFamily: 'Pretendard', + fontWeight: FontWeight.w400, + fontSize: 13, + height: 1.4, + letterSpacing: -0.4, + color: colorScheme.outline, + ), + textAlign: TextAlign.center, ), )); @@ -130,7 +153,7 @@ class TwoActionDialog extends StatelessWidget { content: bodyContent, actions: [ SizedBox( - width: dialogWidth, + width: actionsWidth, child: Row( children: [ if (config.secondaryAction != null) ...[ @@ -158,11 +181,13 @@ class TwoActionDialog extends StatelessWidget { ], actionsAlignment: MainAxisAlignment.center, innerPadding: config.innerPadding, - alignment: Alignment.center, + alignment: config.alignment, + insetPadding: config.insetPadding, titleTextAlign: TextAlign.center, contentTextAlign: TextAlign.center, titleContentSpacing: config.titleContentSpacing, contentActionsSpacing: config.contentActionsSpacing, + constraints: BoxConstraints.tightFor(width: dialogWidth), ); } } diff --git a/lib/presentation/shared/components/two_button_delete_dialog.dart b/lib/presentation/shared/components/two_button_delete_dialog.dart index f2477633..e4756121 100644 --- a/lib/presentation/shared/components/two_button_delete_dialog.dart +++ b/lib/presentation/shared/components/two_button_delete_dialog.dart @@ -9,6 +9,7 @@ Future showTwoButtonDeleteDialog( required String cancelText, required String confirmText, bool barrierDismissible = true, + bool sourceCalendarLayout = false, }) async { final result = await showTwoActionDialog( context, @@ -16,6 +17,17 @@ Future showTwoButtonDeleteDialog( title: title, description: description, barrierDismissible: barrierDismissible, + useSafeArea: !sourceCalendarLayout, + barrierColor: sourceCalendarLayout ? const Color(0x6b000000) : null, + innerPadding: sourceCalendarLayout + ? const EdgeInsets.all(16) + : TwoActionDialogTokens.innerPadding, + titleContentSpacing: sourceCalendarLayout + ? 12 + : TwoActionDialogTokens.titleContentSpacing, + contentActionsSpacing: sourceCalendarLayout + ? 12 + : TwoActionDialogTokens.contentActionsSpacing, secondaryAction: DialogActionConfig( label: cancelText, variant: ModalWideButtonVariant.neutral, diff --git a/lib/presentation/shared/theme/calendar_theme.dart b/lib/presentation/shared/theme/calendar_theme.dart index fda41a7d..812be3dc 100644 --- a/lib/presentation/shared/theme/calendar_theme.dart +++ b/lib/presentation/shared/theme/calendar_theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:table_calendar/table_calendar.dart'; @@ -66,13 +67,16 @@ class CalendarTheme extends ThemeExtension { leftChevronVisible: false, rightChevronVisible: false, headerPadding: EdgeInsets.zero, - leftChevronIcon: Icon( - Icons.chevron_left, - color: colorScheme.outlineVariant, + leftChevronIcon: RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + 'calendar_month_arrow.svg', + package: 'assets', + ), ), - rightChevronIcon: Icon( - Icons.chevron_right, - color: colorScheme.outlineVariant, + rightChevronIcon: SvgPicture.asset( + 'calendar_month_arrow.svg', + package: 'assets', ), titleTextStyle: textTheme.titleMedium!, ), @@ -86,8 +90,9 @@ class CalendarTheme extends ThemeExtension { weekendTextStyle: textTheme.bodyLarge!, defaultTextStyle: textTheme.bodyLarge!, cellMargin: const EdgeInsets.all(2.0), - markerDecoration: BoxDecoration( - color: colorScheme.primary, + markerSize: 6, + markerDecoration: const BoxDecoration( + color: Color(0xff4f69df), shape: BoxShape.circle, ), markerMargin: const EdgeInsets.symmetric(horizontal: 1.0), diff --git a/plans/ios-ui-quality-figma-parity.md b/plans/ios-ui-quality-figma-parity.md new file mode 100644 index 00000000..6e470875 --- /dev/null +++ b/plans/ios-ui-quality-figma-parity.md @@ -0,0 +1,16 @@ +# iOS UI quality and Figma parity + +Updated 2026-09-25. Implementation follows the refreshed `main` screen set at `c7184b2a`, merged into the isolated QA branch at `64bc111d`, with subsequent QA fixes layered on that base. Canonical Figma source: [OnTime Design System](https://www.figma.com/design/FIdR6bUMHScn9FWbwqrBsA/OnTime-Design-System), including Screen Shells, Form Flows, Preparation, and Permission Onboarding pages. The reference viewport is Korean iOS 390×844; Calendar uses its 390×852 source where specified. + +## Execution status + +1. **Complete — inventory and source audit.** [The manifest](../docs/design/figma_code_map.yaml) records 21 routes (16 reviewed, five excluded) and 58 variants (57 reviewed, one excluded): 73 reviewed app entries, zero pending/in-progress entries. Strict manifest validation passes. Exclusions have explicit source/platform reasons. Eleven standalone Schedule Detail designs without an app implementation remain in `design_only_nodes` as unmapped; they are not counted as reviewed app entries. +2. **Complete — source comparison and deterministic fixtures.** Source exports, hashes, Flutter goldens, and runtime adaptations are recorded in six dated reports: [Home/Calendar](../docs/design/review/calendar-parity-20260925.md), [original forms/preparation edit](../docs/design/review/original-form-parity-20260925.md), [recurring flows](../docs/design/review/recurring-parity-20260925.md), [onboarding/permissions](../docs/design/review/onboarding-parity-20260925.md), [recovery/local data/privacy](../docs/design/review/recovery-parity-20260925.md), and [preparation runtime](../docs/design/review/runtime-parity-20260925.md). Each report distinguishes source review from pixel identity and keeps real date arithmetic, persisted values, validation, and native chrome intact. +3. **Complete — UI repairs and targeted behavior/accessibility checks.** Source assets and component geometry are applied, accessible text can grow/scroll, and the four-step creation/edit flow, recurring scope/conflicts/owned preparation, timer completion, recovery, secure backup input, and closed-storage reset lifecycle are retained. Focused verification results and deliberate source exceptions are in the reports above. +4. **Complete for the executed simulator scope — native setup and durable-data flow.** [The native evidence](../docs/design/review/native-20260925/README.md) records real notification/AlarmKit denial/defer, numeric preparation entry, Files export/import, restore, reset, and process relaunch. The exported 811-byte encrypted backup restored buffer 35→30 minutes, and the external file survived reset unchanged. Native testing also exposed and led to repairs for the iOS export API/UTI boundary and duration-row focus. +5. **Final combined verification — recorded centrally.** The sole final full-suite and coverage result is maintained in [the completion report](../docs/design/review/completion-20260925.md#final-combined-validation). Focused passes and the previous checkpoint's totals must not be substituted for that final run. +6. **Physical-device release gate — unavailable.** Paired iPhones/iPad were unavailable. Physical font/layout inspection, VoiceOver operation, background/locked-screen alarm delivery, and frame-time/scroll performance are not certified by simulator or widget results. + +## Delivery boundary + +[Draft PR #596](https://github.com/DevKor-github/OnTime-front/pull/596) remains the review artifact. Source review of the implemented app is complete with documented adaptations; it does not mean all Figma-only screens were implemented or every pixel matches Figma. The physical-device gate and final combined verification remain separate from the completed visual inventory. See [handoff](../handoff/ios-ui-quality-figma-parity.md) for the remaining release acceptance work. diff --git a/pubspec.lock b/pubspec.lock index fa259a1c..fc7d80b4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1266,7 +1266,7 @@ packages: source: hosted version: "6.6.1" yaml: - dependency: transitive + dependency: "direct dev" description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce diff --git a/pubspec.yaml b/pubspec.yaml index a6c3c1a5..145714af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -83,6 +83,7 @@ dev_dependencies: shared_preferences_platform_interface: 2.4.1 flutter_test: sdk: flutter + yaml: ^3.1.3 drift_dev: ^2.31.0 diff --git a/test/presentation/app/bloc/schedule/schedule_bloc_test.dart b/test/presentation/app/bloc/schedule/schedule_bloc_test.dart index ccf11546..8d65fb91 100644 --- a/test/presentation/app/bloc/schedule/schedule_bloc_test.dart +++ b/test/presentation/app/bloc/schedule/schedule_bloc_test.dart @@ -1471,14 +1471,15 @@ void main() { controller.add(scheduleA); await Future.delayed(const Duration(milliseconds: 20)); controller.add(scheduleB); + final startedFuture = bloc.stream.firstWhere( + (s) => s.status == ScheduleStatus.started && s.schedule?.id == 'B', + ); await Future.delayed(const Duration(milliseconds: 220)); expect(bloc.state.schedule?.id, 'B'); expect(bloc.state.status, ScheduleStatus.upcoming); - final started = await bloc.stream.firstWhere( - (s) => s.status == ScheduleStatus.started, - ); + final started = await startedFuture; expect(started.schedule?.id, 'B'); expect(navigationService.pushedRoutes, ['/scheduleStart']); }, diff --git a/test/presentation/schedule_create/preparation_form/preparation_form_list_field_test.dart b/test/presentation/schedule_create/preparation_form/preparation_form_list_field_test.dart index ac3fd619..b8c89018 100644 --- a/test/presentation/schedule_create/preparation_form/preparation_form_list_field_test.dart +++ b/test/presentation/schedule_create/preparation_form/preparation_form_list_field_test.dart @@ -65,7 +65,7 @@ void main() { ); await tester.enterText(find.byType(TextFormField), 'Coffee'); - await tester.tap(find.text('00')); + await tester.tap(find.text('00 min')); await tester.pumpAndSettle(); focusNode.unfocus(); await tester.pump(); @@ -170,7 +170,7 @@ void main() { await tester.enterText(find.byType(TextFormField), 'Shower and pack'); expect(removeCount, 0); - expect(find.text('10'), findsOneWidget); + expect(find.text('10 min'), findsOneWidget); }); } diff --git a/test/presentation/schedule_create/preparation_form/preparation_form_reorderable_list_test.dart b/test/presentation/schedule_create/preparation_form/preparation_form_reorderable_list_test.dart index 0f4f02c2..fb7dbd53 100644 --- a/test/presentation/schedule_create/preparation_form/preparation_form_reorderable_list_test.dart +++ b/test/presentation/schedule_create/preparation_form/preparation_form_reorderable_list_test.dart @@ -60,7 +60,7 @@ void main() { await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField).first, 'Morning shower'); - await tester.tap(find.text('10')); + await tester.tap(find.text('10 min')); await tester.pumpAndSettle(); await tester.tap(find.text('OK')); await tester.pumpAndSettle(); diff --git a/test/presentation/shared/theme/button_and_calendar_theme_test.dart b/test/presentation/shared/theme/button_and_calendar_theme_test.dart index 0bca9929..2daa27c6 100644 --- a/test/presentation/shared/theme/button_and_calendar_theme_test.dart +++ b/test/presentation/shared/theme/button_and_calendar_theme_test.dart @@ -66,7 +66,7 @@ void main() { expect(calendarTheme.calendarStyle.outsideDaysVisible, isFalse); expect( calendarTheme.calendarStyle.markerDecoration, - BoxDecoration(color: colorScheme.primary, shape: BoxShape.circle), + const BoxDecoration(color: Color(0xff4f69df), shape: BoxShape.circle), ); expect( calendarTheme.daysOfWeekStyle.dowTextFormatter!.call( diff --git a/tool/report_figma_ui_coverage.dart b/tool/report_figma_ui_coverage.dart new file mode 100644 index 00000000..868c17c3 --- /dev/null +++ b/tool/report_figma_ui_coverage.dart @@ -0,0 +1,175 @@ +import 'dart:io'; + +import 'package:crypto/crypto.dart'; +import 'package:yaml/yaml.dart'; + +const manifestPath = 'docs/design/figma_code_map.yaml'; +const routerPath = 'lib/presentation/shared/router/go_router.dart'; +const statuses = {'pending', 'in_progress', 'reviewed', 'excluded'}; + +void main(List args) { + final strict = args.contains('--strict'); + final allowMissingGoldens = args.contains('--allow-missing-goldens'); + final manifest = loadYaml(File(manifestPath).readAsStringSync()) as YamlMap; + final entries = manifest['screens'] as YamlList; + final failures = []; + final routes = {}; + final counts = {}; + final variantCounts = {}; + + for (final value in entries) { + final entry = value as YamlMap; + final route = entry['route'] as String?; + final status = entry['status'] as String?; + if (route == null || !routes.add(route)) { + failures.add('Missing or duplicate route: $route'); + continue; + } + if (!statuses.contains(status)) { + failures.add('$route has invalid status: $status'); + continue; + } + counts[status!] = (counts[status] ?? 0) + 1; + + final nodeId = entry['figma_node'] as String?; + if (nodeId != null && !RegExp(r'^\d+:\d+$').hasMatch(nodeId)) { + failures.add('$route has malformed Figma node: $nodeId'); + } + if (status == 'reviewed' && nodeId == null) { + failures.add('$route is reviewed without a Figma node'); + } + if (status == 'excluded' && entry['reason'] == null) { + failures.add('$route is excluded without a reason'); + } + + final imagePath = entry['figma_image'] as String?; + final expectedHash = entry['figma_sha256'] as String?; + final goldenPath = entry['flutter_golden'] as String?; + if ((imagePath == null) != (expectedHash == null)) { + failures.add('$route must pair Figma image with its SHA-256'); + } + if (imagePath != null) { + final image = File(imagePath); + if (!image.existsSync()) { + failures.add('$route is missing Figma image: $imagePath'); + } else if (sha256.convert(image.readAsBytesSync()).toString() != + expectedHash) { + failures.add('$route Figma image hash changed: $imagePath'); + } + } + if (goldenPath != null && + !allowMissingGoldens && + !File(goldenPath).existsSync()) { + failures.add('$route is missing Flutter golden: $goldenPath'); + } + if (status == 'reviewed' && (imagePath == null || goldenPath == null)) { + failures.add('$route is reviewed without both comparison images'); + } + + final variants = entry['variants'] as YamlList?; + final states = {}; + for (final value in variants ?? []) { + final variant = value as YamlMap; + final state = variant['state'] as String?; + final label = '$route/$state'; + final variantStatus = variant['status'] as String?; + if (state == null || !states.add(state)) { + failures.add('$route has a missing or duplicate variant state: $state'); + } + if (!statuses.contains(variantStatus)) { + failures.add('$label has invalid status: $variantStatus'); + continue; + } + variantCounts[variantStatus!] = (variantCounts[variantStatus] ?? 0) + 1; + + final variantNode = variant['figma_node'] as String?; + if (variantNode == null || !RegExp(r'^\d+:\d+$').hasMatch(variantNode)) { + failures.add('$label has missing or malformed Figma node'); + } + if (variantStatus == 'excluded' && variant['reason'] == null) { + failures.add('$label is excluded without a reason'); + } + + final variantImagePath = variant['figma_image'] as String?; + final variantHash = variant['figma_sha256'] as String?; + final variantGoldenPath = variant['flutter_golden'] as String?; + if ((variantImagePath == null) != (variantHash == null)) { + failures.add('$label must pair Figma image with its SHA-256'); + } + if (variantImagePath != null) { + final image = File(variantImagePath); + if (!image.existsSync()) { + failures.add('$label is missing Figma image: $variantImagePath'); + } else if (sha256.convert(image.readAsBytesSync()).toString() != + variantHash) { + failures.add('$label Figma image hash changed: $variantImagePath'); + } + } + if (variantGoldenPath != null && + !allowMissingGoldens && + !File(variantGoldenPath).existsSync()) { + failures.add('$label is missing Flutter golden: $variantGoldenPath'); + } + if (variantStatus == 'reviewed' && + (variantImagePath == null || variantGoldenPath == null)) { + failures.add('$label is reviewed without both comparison images'); + } + } + } + + final router = File(routerPath).readAsStringSync(); + final declaredRoutes = RegExp(r"path:\s*'(/[^']+)'", multiLine: true) + .allMatches(router) + .map((match) => match.group(1)!) + .map((route) => route == '/start' ? '/onboarding/start' : route) + .toSet(); + for (final route in declaredRoutes.difference(routes)) { + failures.add('Router path has no Figma coverage entry: $route'); + } + for (final route in routes.difference(declaredRoutes)) { + failures.add('Coverage entry has no router path: $route'); + } + + stdout.writeln( + 'Figma UI coverage: ${entries.length} routes; ' + 'reviewed ${counts['reviewed'] ?? 0}, ' + 'in progress ${counts['in_progress'] ?? 0}, ' + 'pending ${counts['pending'] ?? 0}, ' + 'excluded ${counts['excluded'] ?? 0}; ' + 'variants reviewed ${variantCounts['reviewed'] ?? 0}, ' + 'in progress ${variantCounts['in_progress'] ?? 0}, ' + 'pending ${variantCounts['pending'] ?? 0}, ' + 'excluded ${variantCounts['excluded'] ?? 0}.', + ); + final designOnlyNodes = manifest['design_only_nodes'] as YamlList?; + final unmappedIds = {}; + for (final value in designOnlyNodes ?? []) { + final node = value as YamlMap; + final id = node['figma_node'] as String?; + if (id == null || + !RegExp(r'^\d+:\d+$').hasMatch(id) || + !unmappedIds.add(id) || + node['status'] != 'unmapped' || + node['reason'] == null) { + failures.add('Invalid or unexplained design-only node: $id'); + } + } + stdout.writeln( + 'Design-only states without an app route: ${unmappedIds.length}. ' + 'These are not counted as reviewed app UI; strict mode checks mapped routes.', + ); + if (strict && + (counts['pending'] ?? 0) + + (counts['in_progress'] ?? 0) + + (variantCounts['pending'] ?? 0) + + (variantCounts['in_progress'] ?? 0) > + 0) { + failures.add( + 'Strict Figma UI coverage requires all routes and variants reviewed or excluded.', + ); + } + for (final failure in failures) { + stderr.writeln(failure); + } + if (failures.isNotEmpty) exitCode = 1; +} diff --git a/widgetbook/lib/alarm_graph_component.dart b/widgetbook/lib/alarm_graph_component.dart index b3e581cb..73575ccd 100644 --- a/widgetbook/lib/alarm_graph_component.dart +++ b/widgetbook/lib/alarm_graph_component.dart @@ -3,10 +3,7 @@ import 'package:widgetbook/widgetbook.dart'; import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; import 'package:on_time_front/presentation/alarm/components/alarm_graph_component.dart'; -@widgetbook.UseCase( - name: 'Static Graph Painter', - type: AlarmGraphComponent, -) +@widgetbook.UseCase(name: 'Static Graph Painter', type: AlarmGraphComponent) Widget alarmGraphComponentUseCase(BuildContext context) { final progress = context.knobs.double.slider( label: 'Progress', @@ -21,7 +18,7 @@ Widget alarmGraphComponentUseCase(BuildContext context) { height: 700, child: Center( child: CustomPaint( - size: const Size(230, 115), + size: const Size.square(268), painter: AlarmGraphComponent( progress: progress, backgroundColor: const Color(0xFF3D54BC),