feat(ux): NoteCard chip affordance 강화 + 헤더 사이드바 토글 + default visible + 창 크기

dogfood 발견 사항 묶음:

- **NotebookChip** 시각 강화 — 청색 배경 + 📓 아이콘 + ▾ caret + dropdown
  헤더 '이동할 노트북'. 클릭 시 다른 노트북 dropdown 명확히 발견 가능.
  다른 노트북 없으면 disabled state.
- **헤더 좌측 ☰ 햄버거 버튼** — 마우스로 사이드바 토글 (Cmd/Ctrl+B 와 동일).
- **사이드바 default visible** — settings.getSidebarVisible 의 default false→true,
  store init 도 동일. 기존 사용자가 명시적으로 false 저장했다면 그 값 유지.
- **inboxWindow 기본 크기 확장** — 900×720 → 1200×800. 사이드바 240px 가
  default 가시화되므로 main 영역 확보.

851 tests pass + typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-15 14:22:57 +09:00
parent da6d296b77
commit d40880de5b
7 changed files with 38 additions and 15 deletions

View File

@@ -186,7 +186,7 @@ export class SettingsService {
// v0.4 Task 15 — sidebar 레이아웃 영속화.
async getSidebarVisible(): Promise<boolean> {
const s = await this.load();
return s.sidebar_visible ?? false;
return s.sidebar_visible ?? true;
}
async setSidebarVisible(v: boolean): Promise<void> {

View File

@@ -22,8 +22,8 @@ export function createInboxWindow(opts: { visible?: boolean } = {}): BrowserWind
}
inboxWindow = new BrowserWindow({
width: 900,
height: 720,
width: 1200,
height: 800,
show: false,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),

View File

@@ -118,6 +118,17 @@ export function App(): React.ReactElement {
<Sidebar />
<main style={{ flex: 1, overflowY: 'auto' }}>
<div className="header">
<button
onClick={() => useInbox.getState().toggleSidebar()}
aria-label="사이드바 토글"
title="사이드바 토글 (Cmd/Ctrl+B)"
style={{
background: 'none', border: 'none', cursor: 'pointer',
fontSize: 18, padding: '0 8px 0 0', color: '#444', lineHeight: 1
}}
>
</button>
<h1 style={{ fontSize: 18, margin: 0 }}>Inkling</h1>
<div style={{ display: 'flex', gap: 6, marginLeft: 12 }}>
{(

View File

@@ -115,29 +115,38 @@ function NotebookChip({ current, notebooks, onMove }: {
onMove: (id: string) => Promise<void>;
}): React.ReactElement {
const [open, setOpen] = useState(false);
const others = notebooks.filter((nb) => nb.id !== current.id);
const hasOthers = others.length > 0;
return (
<span style={{ position: 'relative', display: 'inline-block' }}>
<button
onClick={() => setOpen(!open)}
title="다른 노트북으로 이동"
onClick={() => hasOthers && setOpen(!open)}
title={hasOthers ? '다른 노트북으로 이동' : '현재 노트북'}
disabled={!hasOthers}
style={{
display: 'inline-flex', alignItems: 'center', gap: 4,
background: '#f0f0f0', border: 'none', borderRadius: 10,
padding: '2px 8px', fontSize: 11, cursor: 'pointer'
background: '#eaf3ff', color: '#0a4b80',
border: '1px solid #cfe0f5', borderRadius: 10,
padding: '2px 8px', fontSize: 11, cursor: hasOthers ? 'pointer' : 'default'
}}
>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: current.color ?? '#bbb', display: 'inline-block' }} />
{current.name}
📓 {current.name}
{hasOthers && <span style={{ fontSize: 9, opacity: 0.6 }}></span>}
</button>
{open && (
{open && hasOthers && (
<div
style={{
position: 'absolute', top: '100%', left: 0,
background: '#fff', border: '1px solid #ccc', borderRadius: 4,
zIndex: 50, minWidth: 120, boxShadow: '0 2px 6px rgba(0,0,0,0.15)'
zIndex: 50, minWidth: 140, boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
marginTop: 2
}}
>
{notebooks.filter((nb) => nb.id !== current.id).map((nb) => (
<div style={{ fontSize: 10, color: '#888', padding: '4px 10px', borderBottom: '1px solid #eee' }}>
</div>
{others.map((nb) => (
<button
key={nb.id}
onClick={async () => { await onMove(nb.id); setOpen(false); }}

View File

@@ -129,7 +129,7 @@ export const useInbox = create<InboxState>((set, get) => ({
reviewData: null,
notebooks: [],
selectedNotebookId: null,
sidebarVisible: false,
sidebarVisible: true,
sidebarWidth: 240,
promotionCandidates: [],
async loadInitial() {
@@ -156,7 +156,7 @@ export const useInbox = create<InboxState>((set, get) => ({
notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates,
failedCount, recallCandidate, counts,
ai_enabled: settings.ai_enabled ?? true,
sidebarVisible: settings.sidebar_visible ?? false,
sidebarVisible: settings.sidebar_visible ?? true,
sidebarWidth: settings.sidebar_width ?? 240,
loading: false
});

View File

@@ -166,9 +166,11 @@ describe('App header — 3 tabs (v0.4)', () => {
});
it('Cmd+B 키 이벤트가 toggleSidebar 호출', async () => {
const initialVisible = useInbox.getState().sidebarVisible;
// loadInitial 의 getSettings hydrate 후 state 가 정해진 시점 기준으로 토글 검증.
vi.mocked(inboxApi.getSettings).mockResolvedValue({ onboarding_completed: true, sidebar_visible: false });
render(<App />);
await screen.findByRole('tab', { name: /Inbox/ });
const initialVisible = useInbox.getState().sidebarVisible;
// jsdom 에서 navigator.platform = '' → isMac=false → ctrlKey 로 판단.
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true }));
// toggleSidebar 가 호출되면 sidebarVisible 이 반전됨.

View File

@@ -224,7 +224,8 @@ describe('NoteCard — notebook chip (Task 17)', () => {
fireEvent.click(screen.getByTitle('다른 노트북으로 이동'));
// 현재 nb-1('회사') 는 제외, nb-2('개인') 만 보임.
expect(screen.getByText('개인')).toBeInTheDocument();
expect(screen.queryAllByText('회사').length).toBe(1); // chip 버튼 안에만 존재
// chip 자체 text 는 "📓 회사 ▾" 이라 정확 매칭 X → regex 로 chip 안에만 '회사' 존재 확인.
expect(screen.queryAllByText(/회사/).length).toBe(1);
});
it('dropdown 의 notebook 클릭 → store.moveNoteToNotebook 호출', async () => {