Skip to content

Feat/UI#11

Merged
amemya merged 8 commits into
mainfrom
feat/ui
Apr 27, 2026
Merged

Feat/UI#11
amemya merged 8 commits into
mainfrom
feat/ui

Conversation

@amemya
Copy link
Copy Markdown
Owner

@amemya amemya commented Apr 27, 2026

UIの強化
resolved #6

@amemya amemya self-assigned this Apr 27, 2026
Copilot AI review requested due to automatic review settings April 27, 2026 05:14
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • 新機能

    • フルビューのワークスペース導入:固定トップバー、プレビュー領域、メタデータ用サイドバーを追加。
    • プレビューの空状態にキーボード操作対応とアクセシビリティ属性、読み込みスピナー表示を追加。
    • 操作中は主要ボタンのラベルが切替わり、画像未読込時はエクスポートを非表示に。
  • 改善 / バグ修正

    • 画像選択の多重実行を防止し、選択中はボタンを無効化して状態を安定化。
    • 画像読み込み処理を同期的に安定化し、UI表示の決定性を向上。
  • リファクタ

    • メタデータ入力をサイドバーへ移動し、ラベルと入力を正しく関連付け。
  • スタイル

    • 全体テーマ、ボタン群、入力、空状態、キャンバス表示を刷新しフォーカス可視化を追加。

Walkthrough

アップロード中心の単一画面をフルビューポートのワークスペースに再構築。トップバー、プレビュー領域(空状態含む)、メタデータ用サイドバーを導入し、画像選択に再入防止と明示的な読み込み待ちを追加。フォームのアクセシビリティ属性を整備。

Changes

Cohort / File(s) Summary
スタイル/レイアウト(CSS)
frontend/src/App.css
中央寄せアップロードUIを廃止し、トップバー・ワークスペース・プレビュー・サイドバーのレイアウトへ全面変更。ボタン系(.btn/.btn-primary/.btn-secondary)、フォーム入力、空状態、スピナー、キャンバスラッパー、テーマ変数を追加。旧ヘッダー/アップロード/設定関連の古いルールを削除。
UI構造・選択制御・アクセシビリティ(TypeScript/React)
frontend/src/App.tsx
DOMをトップバー+ワークスペース(preview-areasidebar)へ再編。handleSelectImageに再入防止(ref + state)を追加し、選択中はボタンを無効化。画像のbase64デコードをPromise化してロード完了/エラーを確実に反映。未読込時の空状態にキーボード操作、role="button", aria-busy, aria-disabledを付与。メタデータ入力にid/htmlForを整備。

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as App(UI)
    participant FP as FilePicker (browser)
    participant IMG as Image Decoder

    User->>App: Click "Open/Change Photo"
    App->>FP: open file dialog
    FP-->>App: return File / DataURL
    App->>IMG: create Image(src=DataURL) and await load
    IMG-->>App: onload / onerror
    App->>App: set imageObj, imageLoaded, isSelecting=false
    App-->>User: render preview or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive 「Feat/UI」というタイトルは、UIの改善という変更内容に関連していますが、具体的な変更内容を特定できないほど曖昧で、何が改善されたのかが不明確です。 「Top barの追加、ワークスペースレイアウトの再構成、フォーム入力のアクセシビリティ改善」など、具体的な変更内容を反映したより詳細なタイトルに変更してください。
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed 「UIの強化」という説明は、Issue #6(UIがダサい)を解決する意図に関連しており、PR全体の目的と一致しています。
Linked Issues check ✅ Passed Issue #6の要件「UIの見た目/使い勝手を向上させる」に対して、Top barの追加、ワークスペースレイアウトの再構成、アクセシビリティ改善、新しいボタンシステム、フォーム制御のリスタイリングなど、包括的なUI改善が実装されています。
Out of Scope Changes check ✅ Passed App.tsxの画像読み込みエラーハンドリング改善を含む全ての変更は、Issue #6のUI改善という目的に関連した、スコープ内の変更です。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ui

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the frontend UI layout to address issue #6 (“UIがダサい”), moving the app toward a more modern “workspace” style interface.

Changes:

  • Replaces the centered header/upload layout with a top bar (Open/Export actions) + main workspace.
  • Adds an empty-state preview area and a right-hand sidebar for metadata inputs.
  • Updates App.css to support the new layout, button styles, and panel styling.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
frontend/src/App.tsx Restructures the UI into top-bar/workspace layout, adds empty-state and sidebar inputs.
frontend/src/App.css Adds new styling tokens and layout styles for top bar, workspace, preview area, sidebar, and buttons.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/App.css
max-width: 100%;
max-height: 60vh;
max-height: 100%;
object-fit: contain;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object-fit doesn't apply to <canvas> elements, so object-fit: contain; here is ineffective and can be misleading. Either remove it or replace it with explicit CSS sizing (e.g., controlling width/height) that achieves the intended containment behavior.

Suggested change
object-fit: contain;

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx Outdated
onClick={handleSelectImage}
role="button"
tabIndex={0}
aria-label="Click or press Enter to open a photo"
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-label says "Click or press Enter" but the key handler also supports Space. Please update the label text (or the handler) so the announced instructions match the actual supported keyboard interactions.

Suggested change
aria-label="Click or press Enter to open a photo"
aria-label="Click or press Enter or Space to open a photo"

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx Outdated
Comment on lines +200 to +205
<div
className="empty-state"
onClick={handleSelectImage}
role="button"
tabIndex={0}
aria-label="Click or press Enter to open a photo"
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty state is implemented as a <div role="button">. For better native accessibility (keyboard behavior, semantics, screen reader hints) use a real <button type="button"> and style it to look the same, instead of recreating button behavior manually.

Copilot uses AI. Check for mistakes.
Comment thread frontend/src/App.tsx
Comment on lines +206 to +211
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); // Prevent page scroll for Space
handleSelectImage();
}
}}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onKeyDown will fire repeatedly when a key is held (key repeat), which can trigger multiple OpenImage() calls/file dialogs. Consider guarding against e.repeat, moving this to onKeyUp, or using a native <button> (which avoids most of this manual handling).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/App.css`:
- Around line 147-156: The .sidebar rule currently uses a fixed width of 320px
which breaks on smaller screens; update the CSS to add a media query (e.g.,
`@media` (max-width: 768px)) that overrides .sidebar to width: 100%, removes or
changes the left border (e.g., border-left: none; add border-top instead), and
adjust related layout containers such as .workspace to stack vertically
(flex-direction: column) so the sidebar becomes responsive on tablets and
phones.
- Around line 123-124: Add a prefers-reduced-motion media query that disables
transitions for interactive/animated selectors so users who prefer reduced
motion don't see animations; specifically, target the selectors used near the
transition declaration (e.g., .btn, .empty-state, .input-group input) and set
transition: none (and any related animation properties) inside `@media`
(prefers-reduced-motion: reduce) to override the existing transition: all 0.2s
ease; rule.
- Around line 60-92: Add explicit disabled-state styles for buttons: update the
.btn selector family (.btn, .btn-primary, .btn-secondary) to include rules for
:disabled (and [disabled]) that set cursor: not-allowed, lower opacity (e.g.
0.5), disable pointer-events/hover visual changes, and remove accent
outlines/hover backgrounds (or keep a muted focus-visible) so disabled buttons
visibly differ and do not react to hover/focus the same as active buttons.

In `@frontend/src/App.tsx`:
- Around line 213-217: The decorative SVG in App.tsx (the inline <svg> element
inside the App component JSX) should include aria-hidden="true" to prevent
screen readers from duplicating the already-present parent aria-label; update
the SVG element definition to add the aria-hidden="true" attribute (keep other
attributes unchanged) so the icon is ignored by assistive technologies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e1cc188e-7f89-4288-9eec-058b1e009475

📥 Commits

Reviewing files that changed from the base of the PR and between a212550 and f76f067.

📒 Files selected for processing (2)
  • frontend/src/App.css
  • frontend/src/App.tsx

Comment thread frontend/src/App.css
Comment on lines +60 to +92
.btn {
padding: 0.4rem 1rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.btn:focus-visible,
.empty-state:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}

.main-content {
.btn-secondary {
background-color: transparent;
color: var(--text-primary);
border-color: var(--border-color);
}

.btn-secondary:hover {
background-color: rgba(255, 255, 255, 0.05);
}

.btn-primary {
background-color: var(--accent-color);
color: white;
}

.btn-primary:hover {
background-color: var(--accent-hover);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

ボタンの無効状態スタイルの追加を検討してください。

将来的にボタンを無効化する必要がある場合(例:エクスポート処理中)に備えて、:disabled状態のスタイルを追加することを推奨します。

💅 無効状態のスタイル追加案
 .btn-primary:hover {
     background-color: var(--accent-hover);
 }
+
+.btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.btn {
padding: 0.4rem 1rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.btn:focus-visible,
.empty-state:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
.main-content {
.btn-secondary {
background-color: transparent;
color: var(--text-primary);
border-color: var(--border-color);
}
.btn-secondary:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.btn-primary {
background-color: var(--accent-color);
color: white;
}
.btn-primary:hover {
background-color: var(--accent-hover);
}
.btn {
padding: 0.4rem 1rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.btn:focus-visible,
.empty-state:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
.btn-secondary {
background-color: transparent;
color: var(--text-primary);
border-color: var(--border-color);
}
.btn-secondary:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.btn-primary {
background-color: var(--accent-color);
color: white;
}
.btn-primary:hover {
background-color: var(--accent-hover);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.css` around lines 60 - 92, Add explicit disabled-state
styles for buttons: update the .btn selector family (.btn, .btn-primary,
.btn-secondary) to include rules for :disabled (and [disabled]) that set cursor:
not-allowed, lower opacity (e.g. 0.5), disable pointer-events/hover visual
changes, and remove accent outlines/hover backgrounds (or keep a muted
focus-visible) so disabled buttons visibly differ and do not react to
hover/focus the same as active buttons.

Comment thread frontend/src/App.css
Comment on lines 123 to 124
transition: all 0.2s ease;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

prefers-reduced-motionメディアクエリの追加を検討してください。

アニメーションを減らすことを好むユーザーのために、prefers-reduced-motionメディアクエリを追加することで、アクセシビリティがさらに向上します。

♿ reduced-motion対応の追加案
`@media` (prefers-reduced-motion: reduce) {
    .btn,
    .empty-state,
    .input-group input {
        transition: none;
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.css` around lines 123 - 124, Add a prefers-reduced-motion
media query that disables transitions for interactive/animated selectors so
users who prefer reduced motion don't see animations; specifically, target the
selectors used near the transition declaration (e.g., .btn, .empty-state,
.input-group input) and set transition: none (and any related animation
properties) inside `@media` (prefers-reduced-motion: reduce) to override the
existing transition: all 0.2s ease; rule.

Comment thread frontend/src/App.css
Comment on lines +147 to 156
/* Sidebar */
.sidebar {
width: 320px;
background-color: var(--bg-panel);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 1rem;
box-sizing: border-box;
overflow-y: auto;
flex-shrink: 0;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

レスポンシブ対応の検討をお勧めします。

サイドバーの幅が320pxに固定されています。小さな画面サイズ(タブレットなど)での表示を考慮して、メディアクエリでの調整を検討してください。

📱 レスポンシブ対応の追加案
`@media` (max-width: 768px) {
    .sidebar {
        width: 100%;
        border-left: none;
        border-top: 1px solid var(--border-color);
    }
    
    .workspace {
        flex-direction: column;
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.css` around lines 147 - 156, The .sidebar rule currently
uses a fixed width of 320px which breaks on smaller screens; update the CSS to
add a media query (e.g., `@media` (max-width: 768px)) that overrides .sidebar to
width: 100%, removes or changes the left border (e.g., border-left: none; add
border-top instead), and adjust related layout containers such as .workspace to
stack vertically (flex-direction: column) so the sidebar becomes responsive on
tablets and phones.

Comment thread frontend/src/App.tsx Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/App.tsx`:
- Around line 186-210: Add a reentrancy guard to prevent handleSelectImage from
being invoked concurrently: introduce a boolean React state like isSelecting,
have handleSelectImage immediately return if isSelecting is true, set
isSelecting = true at start and ensure it is set false in every completion path
(use finally or ensure in both success/error branches). Also update UI triggers
(the "Open Photo"/"Change Photo" button, the conditional Export button
activation if needed, and the empty-state onClick/onKeyDown handlers) to be
disabled/no-op while isSelecting is true (e.g., set disabled on the button and
short-circuit the event handlers). Reference: handleSelectImage, imageLoaded,
empty-state click/onKeyDown handlers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ac297066-391a-448b-a06a-5bd41f516099

📥 Commits

Reviewing files that changed from the base of the PR and between f76f067 and 7e3a40b.

📒 Files selected for processing (1)
  • frontend/src/App.tsx

Comment thread frontend/src/App.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/App.tsx (1)

30-74: ⚠️ Potential issue | 🟠 Major

選択ガードの解除タイミングが早すぎて競合が再発します。

Line [71]〜Line [73] で isSelecting を解除していますが、画像デコード完了(Line [58]〜Line [66])前に再操作可能になります。結果として複数選択の onload 完了順で古い画像が後勝ちする可能性があります。デコード完了まで await してから解除してください。

🔧 修正案(デコード完了まで await)
+    const loadImageFromBase64 = (src: string) =>
+        new Promise<HTMLImageElement>((resolve, reject) => {
+            const img = new Image();
+            img.onload = () => resolve(img);
+            img.onerror = () => reject(new Error("Failed to decode or render the selected image"));
+            img.src = src;
+        });
+
     const handleSelectImage = async () => {
         if (isSelectingRef.current) return;
         isSelectingRef.current = true;
         setIsSelecting(true);
         try {
             const result = await OpenImage();
@@
-            // Update EXIF state (leave empty if not found)
-            setExif({
-                camera: result.camera || "",
-                lens: result.lens || "",
-                focalLength: result.focalLength || "",
-                aperture: result.aperture || "",
-                shutterSpeed: result.shutterSpeed || "",
-                iso: result.iso || ""
-            });
-
-            // Load the Base64 image
-            const img = new Image();
-            img.onload = () => {
-                setImageObj(img);
-                setImageLoaded(true);
-            };
-            img.onerror = () => {
-                console.error("Failed to decode or render the selected image");
-                setImageObj(null);
-                setImageLoaded(false);
-            };
-            img.src = result.imageBase64;
+            const img = await loadImageFromBase64(result.imageBase64);
+            setExif({
+                camera: result.camera || "",
+                lens: result.lens || "",
+                focalLength: result.focalLength || "",
+                aperture: result.aperture || "",
+                shutterSpeed: result.shutterSpeed || "",
+                iso: result.iso || ""
+            });
+            setImageObj(img);
+            setImageLoaded(true);
 
         } catch (err) {
             console.error("Failed to open image:", err);
+            setImageObj(null);
+            setImageLoaded(false);
         } finally {
             isSelectingRef.current = false;
             setIsSelecting(false);
         }
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.tsx` around lines 30 - 74, The selection guard is being
released in the finally block before the image decode/render finishes
(img.onload/img.onerror), allowing concurrent selections to race; modify
handleSelectImage to await image decoding by wrapping the img.onload/img.onerror
sequence in a Promise (e.g., create a loadPromise that resolves on onload and
rejects on onerror), await that promise before reaching the finally/unsetting
isSelectingRef.current and setIsSelecting(false), and continue using
setImageObj/setImageLoaded inside the promise handlers so the guard only clears
after the image has successfully loaded or failed.
♻️ Duplicate comments (3)
frontend/src/App.css (3)

141-147: ⚠️ Potential issue | 🟡 Minor

prefers-reduced-motion の考慮を追加してください。

.spinner の常時アニメーションと各種 transition に対して、@media (prefers-reduced-motion: reduce) で無効化オーバーライドを入れるとアクセシビリティが改善します。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.css` around lines 141 - 147, Add a media query for users who
prefer reduced motion and override the spinner animation and any related
transitions: specifically target the `@keyframes` spin consumer (.spinner) and set
animation: none (and animation-duration/iteration if desired) and also disable
transition (transition: none) inside `@media` (prefers-reduced-motion: reduce) to
ensure the spin is stopped for accessibility.

60-92: ⚠️ Potential issue | 🟡 Minor

無効化ボタンの見た目と挙動を明示してください。

disabled時の専用スタイルがないため、通常/hoverとの差が弱く、cursor: pointerも残ります。.btn:disabled(および[disabled])で視覚差分と非操作性を明示してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.css` around lines 60 - 92, The CSS lacks disabled-state
rules so disabled buttons still look interactive; add selectors for
.btn:disabled and .btn[disabled] to clearly show non-interactive state (reduce
opacity or use muted background/border), remove or override cursor: pointer (set
cursor: default or not-allowed), and ensure both .btn-primary and .btn-secondary
disabled variants are covered (e.g., muted colors, no hover effects) so disabled
buttons are visually distinct and non-clickable; also consider matching
focus/aria-disabled behavior for .empty-state if it can be disabled.

166-174: ⚠️ Potential issue | 🟡 Minor

サイドバー固定幅のレスポンシブ対応が必要です。

.sidebar { width: 320px; } 固定のままだと狭い画面でワークスペースが圧迫されます。ブレークポイントで .workspace の縦積み化と .sidebar の幅調整を追加してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.css` around lines 166 - 174, The .sidebar uses a fixed width
(320px) which breaks small screens; update the CSS to be responsive by replacing
the hard width with a flexible rule (e.g., use max-width or a flex-basis like
flex: 0 0 320px) and add media queries (breakpoints) that at narrow widths
change .workspace to column stacking (flex-direction: column) and adjust
.sidebar to a narrower or full-width value (e.g., width: 100% or max-width:
240px) and ensure overflow-y behavior remains correct; target the .sidebar and
.workspace selectors in the stylesheet and add the media-query overrides so the
layout stacks and sidebar shrinks on small viewports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@frontend/src/App.tsx`:
- Around line 30-74: The selection guard is being released in the finally block
before the image decode/render finishes (img.onload/img.onerror), allowing
concurrent selections to race; modify handleSelectImage to await image decoding
by wrapping the img.onload/img.onerror sequence in a Promise (e.g., create a
loadPromise that resolves on onload and rejects on onerror), await that promise
before reaching the finally/unsetting isSelectingRef.current and
setIsSelecting(false), and continue using setImageObj/setImageLoaded inside the
promise handlers so the guard only clears after the image has successfully
loaded or failed.

---

Duplicate comments:
In `@frontend/src/App.css`:
- Around line 141-147: Add a media query for users who prefer reduced motion and
override the spinner animation and any related transitions: specifically target
the `@keyframes` spin consumer (.spinner) and set animation: none (and
animation-duration/iteration if desired) and also disable transition
(transition: none) inside `@media` (prefers-reduced-motion: reduce) to ensure the
spin is stopped for accessibility.
- Around line 60-92: The CSS lacks disabled-state rules so disabled buttons
still look interactive; add selectors for .btn:disabled and .btn[disabled] to
clearly show non-interactive state (reduce opacity or use muted
background/border), remove or override cursor: pointer (set cursor: default or
not-allowed), and ensure both .btn-primary and .btn-secondary disabled variants
are covered (e.g., muted colors, no hover effects) so disabled buttons are
visually distinct and non-clickable; also consider matching focus/aria-disabled
behavior for .empty-state if it can be disabled.
- Around line 166-174: The .sidebar uses a fixed width (320px) which breaks
small screens; update the CSS to be responsive by replacing the hard width with
a flexible rule (e.g., use max-width or a flex-basis like flex: 0 0 320px) and
add media queries (breakpoints) that at narrow widths change .workspace to
column stacking (flex-direction: column) and adjust .sidebar to a narrower or
full-width value (e.g., width: 100% or max-width: 240px) and ensure overflow-y
behavior remains correct; target the .sidebar and .workspace selectors in the
stylesheet and add the media-query overrides so the layout stacks and sidebar
shrinks on small viewports.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7302ff52-5222-4603-b026-afe05c9d0401

📥 Commits

Reviewing files that changed from the base of the PR and between 7e3a40b and 15901fd.

📒 Files selected for processing (2)
  • frontend/src/App.css
  • frontend/src/App.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/App.tsx`:
- Around line 56-68: On image decode failure the onerror handler immediately
clears state (setImageObj / setImageLoaded) which removes an existing preview;
change the logic so the onerror does not mutate existing state and only the
onload commits updates: in the Image promise (created in the async block) remove
calls to setImageObj(null) and setImageLoaded(false) from img.onerror and
instead reject without touching state, and ensure only img.onload calls
setImageObj(img) and setImageLoaded(true) so the current preview remains if the
new image fails to decode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c1013e0b-ade4-4a15-ad02-56845e4f7251

📥 Commits

Reviewing files that changed from the base of the PR and between 15901fd and 647af56.

📒 Files selected for processing (1)
  • frontend/src/App.tsx

Comment thread frontend/src/App.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
frontend/src/App.tsx (1)

46-69: ⚠️ Potential issue | 🟠 Major

EXIF と画像は成功時にまとめて反映してください。

Line 47-54 の setExif がデコード完了前に走るため、Change Photo 中に新画像の読み込みが失敗すると、古いプレビューに新しい EXIF だけが残ります。成功時に ExifDataHTMLImageElement をまとめてコミットしないと、失敗時の不整合と読み込み中の一時的な誤表示が発生します。

🔧 修正案
-            // Update EXIF state (leave empty if not found)
-            setExif({
-                camera: result.camera || "",
-                lens: result.lens || "",
-                focalLength: result.focalLength || "",
-                aperture: result.aperture || "",
-                shutterSpeed: result.shutterSpeed || "",
-                iso: result.iso || ""
-            });
-
-            // Load the Base64 image and await its decoding
-            await new Promise<void>((resolve, reject) => {
+            const nextExif: ExifData = {
+                camera: result.camera || "",
+                lens: result.lens || "",
+                focalLength: result.focalLength || "",
+                aperture: result.aperture || "",
+                shutterSpeed: result.shutterSpeed || "",
+                iso: result.iso || ""
+            };
+
+            const nextImage = await new Promise<HTMLImageElement>((resolve, reject) => {
                 const img = new Image();
                 img.onload = () => {
-                    setImageObj(img);
-                    setImageLoaded(true);
-                    resolve();
+                    resolve(img);
                 };
                 img.onerror = () => {
                     console.error("Failed to decode or render the selected image");
                     reject(new Error("Failed to decode image"));
                 };
                 img.src = result.imageBase64;
             });
+
+            setExif(nextExif);
+            setImageObj(nextImage);
+            setImageLoaded(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.tsx` around lines 46 - 69, The EXIF state (setExif) is being
committed before the image decode finishes, causing mismatches if image loading
fails; change the flow so you only update both EXIF and image state together
after the image successfully loads: move the setExif call into the img.onload
handler (or resolve with both result and image and then call setExif,
setImageObj, setImageLoaded together), and ensure img.onerror does not mutate
state (keep previous preview) and rejects using result.imageBase64’s decode
failure; key symbols to update are setExif, setImageObj, setImageLoaded,
img.onload/img.onerror and result.imageBase64.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@frontend/src/App.tsx`:
- Around line 46-69: The EXIF state (setExif) is being committed before the
image decode finishes, causing mismatches if image loading fails; change the
flow so you only update both EXIF and image state together after the image
successfully loads: move the setExif call into the img.onload handler (or
resolve with both result and image and then call setExif, setImageObj,
setImageLoaded together), and ensure img.onerror does not mutate state (keep
previous preview) and rejects using result.imageBase64’s decode failure; key
symbols to update are setExif, setImageObj, setImageLoaded,
img.onload/img.onerror and result.imageBase64.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f3a4555e-6be8-4c46-beba-ea5264c8fd4a

📥 Commits

Reviewing files that changed from the base of the PR and between 647af56 and 24bcb3d.

📒 Files selected for processing (1)
  • frontend/src/App.tsx

@amemya amemya merged commit 598547a into main Apr 27, 2026
1 check passed
@amemya amemya deleted the feat/ui branch April 27, 2026 12:43
@amemya amemya mentioned this pull request Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UIがダサい

2 participants