Task 22 of the slice plan. Wires the Inbox window's renderer: - index.html (replaces the placeholder shipped in Task 2) with the full layout styles + module script. - api.ts re-exports window.inkling.inbox as a typed InboxApi. - recoveryToast.ts persists per-day toast dismissal in localStorage (KST date key) so the banner never re-fires after the user closes it. - store.ts: zustand store with notes, continuity, pendingCount, ollamaStatus, loadInitial(), refreshMeta(), upsert/remove. - main.tsx mounts <App />. - App.tsx orchestrates loadInitial + onNoteUpdated subscription + window-focus refresh, renders header / banners / note list. - 7 component stubs (NoteCard / EditableField / IntentBanner / RecoveryToast / ContinuityBadge / PendingBanner / OllamaBanner) so the shell typechecks today; Tasks 23-28 swap each in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
54 lines
2.0 KiB
TypeScript
54 lines
2.0 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useInbox } from './store.js';
|
|
import { inboxApi } from './api.js';
|
|
import { isRecoveryDismissedToday, markRecoveryDismissed } from './recoveryToast.js';
|
|
import { NoteCard } from './components/NoteCard.js';
|
|
import { ContinuityBadge } from './components/ContinuityBadge.js';
|
|
import { PendingBanner } from './components/PendingBanner.js';
|
|
import { OllamaBanner } from './components/OllamaBanner.js';
|
|
import { RecoveryToast } from './components/RecoveryToast.js';
|
|
|
|
export function App(): React.ReactElement {
|
|
const { notes, loading, loadInitial, refreshMeta, upsertNote, removeNote, continuity } = useInbox();
|
|
const [recoveryDismissed, setRecoveryDismissed] = useState(isRecoveryDismissedToday());
|
|
|
|
useEffect(() => {
|
|
void loadInitial();
|
|
const unsub = inboxApi.onNoteUpdated((note) => {
|
|
upsertNote(note);
|
|
void refreshMeta();
|
|
});
|
|
const onFocus = () => { void refreshMeta(); };
|
|
window.addEventListener('focus', onFocus);
|
|
return () => { unsub(); window.removeEventListener('focus', onFocus); };
|
|
}, [loadInitial, refreshMeta, upsertNote]);
|
|
|
|
const showRecovery = continuity.showRecoveryToast && !recoveryDismissed;
|
|
|
|
return (
|
|
<>
|
|
<div className="header">
|
|
<h1 style={{ fontSize: 18, margin: 0 }}>Inkling</h1>
|
|
<ContinuityBadge />
|
|
</div>
|
|
<main className="main">
|
|
<OllamaBanner />
|
|
<RecoveryToast
|
|
show={showRecovery}
|
|
onDismiss={() => { markRecoveryDismissed(); setRecoveryDismissed(true); }}
|
|
/>
|
|
<PendingBanner />
|
|
{loading && notes.length === 0 ? (
|
|
<div className="empty">불러오는 중…</div>
|
|
) : notes.length === 0 ? (
|
|
<div className="empty">첫 기억을 구출해보세요. <code>Ctrl+Shift+J</code></div>
|
|
) : (
|
|
notes.map((n) => (
|
|
<NoteCard key={n.id} note={n} onDeleted={() => removeNote(n.id)} onUpdated={(u) => upsertNote(u)} />
|
|
))
|
|
)}
|
|
</main>
|
|
</>
|
|
);
|
|
}
|