diff --git a/.cline-context.md b/.cline-context.md index 46e74f7..f6f1b74 100644 --- a/.cline-context.md +++ b/.cline-context.md @@ -30,6 +30,12 @@ - Automated Gitea Actions CI workflow (`.gitea/workflows/docker-build.yaml`) for building & pushing container images to Gitea Container Registry with lowercase repository name normalization (`env.REPO`) and OCI source labels (`org.opencontainers.image.source`) for automatic repository package linkage. - Completely revamped `README.md` to reflect universal 2-file Docker deployment for general users, Watchtower auto-update, architecture, and open-standard CalDAV sync guidelines. - Enhanced Task Row UX & Interactive Undo Toast (v0.6.3): Notion-style 6-dot drag handle (`⋮⋮`), DND reordering, `F2` inline editing, right-aligned subtask button, and 6-second floating Undo toast banner. + - Completely redesigned Left Sidebar UI/UX (v0.7.0): Brand checkmark SVG logo, centered matte search box (Ctrl+K), bottom-docked user card, fixed list redirect bug on trash selection, and synchronized live trash counter. + - Minimal & Matte Kanban View Switcher (v0.7.0): Compact single toggle visible only when Kanban is enabled in Labs, with seamless real-time switching. + - Markdown Note Editor defaulting to Preview mode with one-touch toggle button (v0.7.0). + - Real-time Optimistic UI updates for subtask inline addition & checklist checkbox clicks without requiring page refreshes (v0.7.0). + - Auto-promotion of the first-ever registered user to ADMIN role on self-hosted instances (v0.7.0). + - Full N-depth tree drag-and-drop (DND) reordering for main tasks and subtasks with LocalStorage persistence (v0.7.0). - Validated that `npm run lint` and `npm run build` execute with 0 errors and 0 warnings. - **Next Steps (Todo):** diff --git a/AGENTS.md b/AGENTS.md index 10fec20..ac8c752 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,8 @@ src/ | `checkpoint-v0.6.1` | CalDAV / DAVx⁵ 양방향 동기화 프로토콜 고도화(OPTIONS, PROPFIND, REPORT, PUT, DELETE), 설정 모달 내 플랫폼별(Android DAVx⁵ / Apple Reminders / Thunderbird) 인터랙티브 가이드 및 실시간 엔드포인트 테스트 핑, 다국어 i18n 동기화 | 2026-08-21 | | `checkpoint-v0.6.2` | 표준 태스크 내보내기(Export) 기능 구현: TickTick/RFC 4180 호환 CSV (UTF-8 BOM 포함) 및 RFC 5545 iCalendar (`.ics` VTODO), 사이드바 프로필 메뉴 연동, 데모 모드 로컬 Blob 내보내기 지원, 다국어 사전 동기화 | 2026-08-21 | | `checkpoint-v0.6.3` | 노션 스타일 6도트 드래그 핸들(`⋮⋮`) & DND 태스크 재정렬, F2 단축키 인라인 제목 수정, 우측 호버 서브태스크 추가 버튼, 6초 플로팅 인터랙티브 실행 취소(Undo) 토스트 배너 탑재, 휴지통 및 로컬 스토리지 트리 중복 버그 수정 | 2026-08-21 | -| `checkpoint-v0.6.4` | **(현재 최신)** i18n 모듈러 사전 아키텍처(`types.ts`, `locales/en.ts`, `locales/ko.ts`, `locales/ja.ts`) 분리 및 번역 완성도 100%, 어드민 콘솔(`/admin`) 데모 프리뷰 및 멀티유저 게이트 연동, 설정 모달 Admin 탭 정보 고도화, 사이드바 프로필 메뉴 내 Admin Console 진입점 추가 | 2026-08-21 | +| `checkpoint-v0.6.4` | i18n 모듈러 사전 아키텍처(`types.ts`, `locales/en.ts`, `locales/ko.ts`, `locales/ja.ts`) 분리 및 번역 완성도 100%, 어드민 콘솔(`/admin`) 데모 프리뷰 및 멀티유저 게이트 연동, 설정 모달 Admin 탭 정보 고도화, 사이드바 프로필 메뉴 내 Admin Console 진입점 추가 | 2026-08-21 | +| `checkpoint-v0.7.0` | **(현재 최신)** 좌측 사이드바 UI/UX 전면 리디자인(브랜드 로고 SVG, 중앙정렬 매트 검색창, 바닥 밀착 프로필), 뷰 전환(리스트/칸반) 미니멀 원터치 토글 및 실험실 연동 버그 수정, 휴지통 카운트 실시간 동기화 & 기본화면 튕김 버그 수정, 첫 가입 계정 자동 ADMIN 승격, 서브태스크 인라인 추가 및 체크박스 Optimistic UI 실시간 렌더링, 메모장 기본 Preview 모드 & 원터치 토글, N-depth 계층형 DND 드래그 앤 드롭 재정렬 지원 | 2026-08-21 | --- diff --git a/src/app/globals.css b/src/app/globals.css index 8e4432a..1df959d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -459,9 +459,30 @@ a { color: inherit; text-decoration: none; } color: var(--accent); } +.sidebar { + width: var(--sidebar-w, var(--sidebar-width)); + min-width: var(--sidebar-w, var(--sidebar-width)); + background: var(--bg-secondary); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + position: relative; + transition: width var(--dur-normal) var(--ease-out); +} + +.sidebar-search-box:hover { + border-color: var(--accent) !important; + background: var(--bg-primary) !important; + box-shadow: 0 0 0 2px var(--accent-light); +} + .sidebar-footer { + margin-top: auto; padding: 8px 8px 12px; border-top: 1px solid var(--border); + background: var(--bg-secondary); } .user-card { @@ -471,10 +492,15 @@ a { color: inherit; text-decoration: none; } padding: 8px 10px; border-radius: var(--radius-sm); cursor: pointer; - transition: background var(--dur-fast); + transition: all 0.15s cubic-bezier(0.16, 1, 0.3, 1); + border: 1px solid transparent; } -.user-card:hover { background: var(--bg-hover); } +.user-card:hover { + background: var(--bg-primary); + border-color: var(--border); + box-shadow: var(--shadow-xs); +} .user-avatar { width: 32px; diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 6226882..e1ce0d3 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -483,6 +483,7 @@ export function AppShell({ user, isDemo = false }: AppShellProps) { onTagSelect={handleTagSelect} isTrashActive={isTrashActive} onTrashSelect={handleTrashSelect} + trashCount={isDemo && typeof window !== "undefined" ? getAllTrashTasks(getDemoStore().tasks).length : undefined} mobileOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} isDemo={isDemo} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index b809ffe..e4ae43e 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -24,6 +24,7 @@ interface SidebarProps { onTagSelect: (tag: string | null) => void; isTrashActive: boolean; onTrashSelect: () => void; + trashCount?: number; mobileOpen: boolean; onClose: () => void; isDemo?: boolean; @@ -48,6 +49,7 @@ export function Sidebar({ onTagSelect, isTrashActive, onTrashSelect, + trashCount: externalTrashCount, mobileOpen, onClose: _onClose, isDemo = false, @@ -73,7 +75,7 @@ export function Sidebar({ const [exportIncludeCompleted, setExportIncludeCompleted] = useState(true); const [exporting, setExporting] = useState(false); const [tags, setTags] = useState(() => (typeof window !== "undefined" ? getCustomTags() : [])); - const [trashCount, setTrashCount] = useState(() => { + const [internalTrashCount, setInternalTrashCount] = useState(() => { if (typeof window !== "undefined") { const store = getDemoStore(); return getAllTrashTasks(store.tasks).length; @@ -81,6 +83,8 @@ export function Sidebar({ return 0; }); + const trashCount = externalTrashCount !== undefined ? externalTrashCount : internalTrashCount; + // Context Menu for Lists const [contextMenu, setContextMenu] = useState<{ x: number; y: number; listId: string } | null>(null); @@ -91,11 +95,6 @@ export function Sidebar({ const [renamingListId, setRenamingListId] = useState(null); const [renameValue, setRenameValue] = useState(""); - // Search expansion - const [searchExpanded, setSearchExpanded] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); - const searchInputRef = useRef(null); - const inputRef = useRef(null); const fileRef = useRef(null); @@ -103,9 +102,11 @@ export function Sidebar({ setTags(getCustomTags()); const store = getDemoStore(); const trashed = getAllTrashTasks(store.tasks); - setTrashCount(trashed.length); + setInternalTrashCount(trashed.length); }, [lists]); + const initialListSelectedRef = useRef(false); + useEffect(() => { if (!isDemo) { fetch("/api/lists") @@ -113,23 +114,20 @@ export function Sidebar({ .then((data) => { if (Array.isArray(data)) { setLists(data); - if (!selectedListId && data.length > 0) onListSelect(data[0].id); + if (!initialListSelectedRef.current && !selectedListId && !isTrashActive && !selectedTag && data.length > 0) { + initialListSelectedRef.current = true; + onListSelect(data[0].id); + } } }) .catch((err) => console.error("Failed to load lists", err)); } - }, [isDemo, onListSelect, selectedListId, setLists]); + }, [isDemo, onListSelect, selectedListId, isTrashActive, selectedTag, setLists]); useEffect(() => { if (showNewList) setTimeout(() => inputRef.current?.focus(), 50); }, [showNewList]); - useEffect(() => { - if (searchExpanded) { - setTimeout(() => searchInputRef.current?.focus(), 60); - } - }, [searchExpanded]); - // Dismiss undo toast on unmount useEffect(() => { return () => { @@ -422,91 +420,41 @@ export function Sidebar({ return (