+ {/* Header */}
+
+
+ {/* API Key input (collapsible) */}
+ {showKeyInput && (
+
+ {
+ setGeminiKey(e.target.value);
+ localStorage.setItem('guard-gemini-key', e.target.value);
+ }}
+ style={{ flex: 1, background: 'var(--surface2)', border: '1px solid var(--border)', borderRadius: 8, padding: '6px 12px', color: 'var(--text)', fontSize: 13, outline: 'none' }}
+ />
+
+
+ )}
+
+ {/* Main */}
+
+ {state.status === 'idle' && (
+
+ )}
+ {state.status === 'scanning' && (
+
+ )}
+ {state.status === 'results' && (
+ runScan(state.rootDir)}
+ />
+ )}
+
+
+ );
+}
+
+// ── Sub views ────────────────────────────────────────────────────────────────
+function IdleView({ onSelect }: { onSelect: () => void }) {
+ return (
+
+
+
+ {danger > 0 && }
+ {warning > 0 && }
+ {issues.length === 0 && 問題は見つかりませんでした}
+
+
+
+
+
+ {issues.map((issue, idx) => (
+ onApplyFix(issue, rootDir)} />
+ ))}
+
+
+ );
+}
+
+function IssueCard({ issue, onApplyFix }: { issue: ScanIssue; onApplyFix: () => void }) {
+ const [expanded, setExpanded] = useState(false);
+ const [copied, setCopied] = useState(false);
+ const color = issue.severity === 'danger' ? 'var(--danger)' : issue.severity === 'warning' ? 'var(--warning)' : 'var(--info)';
+ const dim = issue.severity === 'danger' ? 'var(--danger-dim)' : issue.severity === 'warning' ? 'var(--warning-dim)' : 'var(--info-dim)';
+ const label = issue.severity === 'danger' ? '危険' : issue.severity === 'warning' ? '注意' : '改善';
+
+ const copyPrompt = async () => {
+ const loc = issue.line ? `${issue.file}(${issue.line}行目付近)` : issue.file;
+ const text = [
+ '以下のセキュリティ問題を修正してください。',
+ '',
+ `ファイル: ${loc}`,
+ `問題: ${issue.title}`,
+ `詳細: ${issue.description}`,
+ ...(issue.fix ? [`修正案: ${issue.fix}`] : []),
+ '',
+ '該当箇所のコードを貼り付けて、上記の問題を安全に修正してください。',
+ ].join('\n');
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1800);
+ };
+
+ return (
+