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:
@@ -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> {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 }}>
|
||||
{(
|
||||
|
||||
@@ -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); }}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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 이 반전됨.
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user