refactor(v026): #24+#41 Banner shared component (severity prop)

4 banner inline style 중복 (warning 황색 / error 적색 / info 청색)
→ <Banner severity="warning|error|info"> wrapper. THEMES map 단일 source.

- ExpiryBanner: warning
- OllamaBanner: warning
- FailedBanner: error
- RecallBanner: info

OllamaSettingsModal 은 modal 형식이라 banner 와 분리 (별개 inline style 유지).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
altair823
2026-05-05 01:42:16 +09:00
parent 476a519fb5
commit 0447b69b82
5 changed files with 59 additions and 34 deletions

View File

@@ -0,0 +1,27 @@
import React from 'react';
/**
* v0.2.6 #24+#41 — 4 banner 의 inline style 중복 제거. severity 별 theme map.
*/
const THEMES = {
warning: { bg: '#fff7e6', border: '#d99500', text: '#946100' },
error: { bg: '#fce4e4', border: '#a33', text: '#a33' },
info: { bg: '#e8f0fe', border: '#4a7ec0', text: '#234' }
} as const;
interface Props {
severity: 'warning' | 'error' | 'info';
children: React.ReactNode;
}
export function Banner({ severity, children }: Props): React.ReactElement {
const t = THEMES[severity];
return (
<div style={{
background: t.bg, border: `1px solid ${t.border}`, color: t.text,
borderRadius: 6, padding: '8px 12px', margin: '8px 0', fontSize: 13
}}>
{children}
</div>
);
}

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import type { Note } from '@shared/types';
import { useInbox } from '../store.js';
import { Banner } from './Banner.js';
export function ExpiryBanner(): React.ReactElement | null {
const candidates = useInbox((s) => s.expiredCandidates);
@@ -72,10 +73,7 @@ function ExpiryBannerInner({ candidates, onTrash, onSnooze }: InnerProps): React
}
return (
<div style={{
background: '#fff7e6', border: '1px solid #d99500', borderRadius: 6,
padding: '8px 12px', margin: '8px 0', fontSize: 13
}}>
<Banner severity="warning">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span> <b> {candidates.length}</b></span>
<button
@@ -152,6 +150,6 @@ function ExpiryBannerInner({ candidates, onTrash, onSnooze }: InnerProps): React
</button>
</>
)}
</div>
</Banner>
);
}

View File

@@ -1,32 +1,31 @@
import React from 'react';
import { useInbox } from '../store.js';
import { Banner } from './Banner.js';
export function FailedBanner(): React.ReactElement | null {
const count = useInbox((s) => s.failedCount);
const retryAllFailed = useInbox((s) => s.retryAllFailed);
if (count === 0) return null;
return (
<div style={{
background: '#fce4e4', border: '1px solid #a33', borderRadius: 6,
padding: '8px 12px', margin: '8px 0', fontSize: 13,
display: 'flex', alignItems: 'center', gap: 8
}}>
<span style={{ flex: 1 }}> AI <b>{count}</b></span>
<button
onClick={() => {
retryAllFailed().catch((e) => {
// eslint-disable-next-line no-console
console.warn('retryAllFailed failed', e);
});
}}
style={{
background: '#a33', color: '#fff',
border: 'none', borderRadius: 4,
padding: '4px 12px', fontSize: 12, cursor: 'pointer'
}}
>
</button>
</div>
<Banner severity="error">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ flex: 1 }}> AI <b>{count}</b></span>
<button
onClick={() => {
retryAllFailed().catch((e) => {
// eslint-disable-next-line no-console
console.warn('retryAllFailed failed', e);
});
}}
style={{
background: '#a33', color: '#fff',
border: 'none', borderRadius: 4,
padding: '4px 12px', fontSize: 12, cursor: 'pointer'
}}
>
</button>
</div>
</Banner>
);
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { useInbox } from '../store.js';
import { Banner } from './Banner.js';
interface OllamaBannerProps {
onOpenSettings?: () => void;
@@ -14,7 +15,8 @@ export function OllamaBanner({ onOpenSettings }: OllamaBannerProps = {}): React.
? '`ollama pull gemma4:e4b` 실행 후 앱을 재시작해주세요.'
: 'Inkling 정리가 잠시 멈췄습니다. Ollama를 실행해주세요.';
return (
<div className="banner warn" style={{ flexDirection: 'column', alignItems: 'flex-start' }}>
<Banner severity="warning">
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
<span style={{ flex: 1 }}> {message}</span>
<button
@@ -51,6 +53,7 @@ export function OllamaBanner({ onOpenSettings }: OllamaBannerProps = {}): React.
: {status.reason}
</span>
) : null}
</div>
</div>
</Banner>
);
}

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { useInbox } from '../store.js';
import { inboxApi } from '../api.js';
import { Banner } from './Banner.js';
export function RecallBanner(): React.ReactElement | null {
const candidate = useInbox((s) => s.recallCandidate);
@@ -47,10 +48,7 @@ export function RecallBanner(): React.ReactElement | null {
}
return (
<div style={{
background: '#e8f0fe', border: '1px solid #4a7ec0', borderRadius: 6,
padding: '8px 12px', margin: '8px 0', fontSize: 13
}}>
<Banner severity="info">
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span>💭 <b> </b></span>
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: '#234' }}>
@@ -90,7 +88,7 @@ export function RecallBanner(): React.ReactElement | null {
</button>
</div>
</div>
</Banner>
);
}