From 97cbef13b12cb3446f8ef063a60fb99140609277 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 11:24:08 +0900 Subject: [PATCH 01/20] add turbo command to desktop2 --- apps/desktop2/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 458c3ccdb..2fff8d8ee 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -9,7 +9,9 @@ "preview": "vite preview", "tauri": "tauri", "typecheck": "tsc --noEmit", - "test": "vitest run" + "test": "vitest run", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" }, "dependencies": { "@ai-sdk/openai-compatible": "^1.0.22", From 8ec9cc8ea8436919bb9960bc31ed65c7d930636a Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 11:27:15 +0900 Subject: [PATCH 02/20] add pre-defined colors --- apps/desktop2/tailwind.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/desktop2/tailwind.config.ts b/apps/desktop2/tailwind.config.ts index 5403a4290..eb97cceed 100644 --- a/apps/desktop2/tailwind.config.ts +++ b/apps/desktop2/tailwind.config.ts @@ -10,6 +10,12 @@ const config = { fontFamily: { "racing-sans": ["Racing Sans One", "cursive"], }, + colors: { + color1: "#FBFBFB", + color2: "#F4F4F4", + color3: "#8e8e8e", + color4: "#484848", + }, }, }, plugins: [], From b3e04d5813ad397396ca280aa7f5eced52747c2f Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 11:27:56 +0900 Subject: [PATCH 03/20] set left sidebar width to 280px --- apps/desktop2/src/components/main/sidebar/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop2/src/components/main/sidebar/index.tsx b/apps/desktop2/src/components/main/sidebar/index.tsx index 0a26c8e4e..a7d5e4605 100644 --- a/apps/desktop2/src/components/main/sidebar/index.tsx +++ b/apps/desktop2/src/components/main/sidebar/index.tsx @@ -14,7 +14,7 @@ export function LeftSidebar() { const showSearchResults = query.trim() !== ""; return ( -
+
Date: Tue, 14 Oct 2025 11:28:28 +0900 Subject: [PATCH 04/20] change tab area ui - just appearance --- apps/desktop2/src/components/main/body/index.tsx | 4 ++-- apps/desktop2/src/components/main/body/shared.tsx | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index fa5926b1b..093d7a36c 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -101,12 +101,12 @@ function Header({ tabs }: { tabs: Tab[] }) { "flex items-center justify-center", "h-full", "mx-1 px-1.5", - "border border-gray-400 rounded-lg", + "rounded-lg", "bg-white hover:bg-gray-50", "transition-colors", ])} > - +
diff --git a/apps/desktop2/src/components/main/body/shared.tsx b/apps/desktop2/src/components/main/body/shared.tsx index 47dc113c1..34b80bbb0 100644 --- a/apps/desktop2/src/components/main/body/shared.tsx +++ b/apps/desktop2/src/components/main/body/shared.tsx @@ -23,9 +23,8 @@ export function TabItemBase( className={clsx([ "flex items-center gap-2 cursor-pointer group", "min-w-[100px] max-w-[200px] h-full px-2", - active - ? "bg-background text-foreground rounded-lg border" - : "bg-muted/50 hover:bg-muted text-muted-foreground rounded-lg border", + "bg-color1 rounded-lg border", + active ? "text-black border-black" : "text-color3 border-transparent", ])} >
@@ -42,8 +41,8 @@ export function TabItemBase( className={clsx([ "text-xs flex-shrink-0 transition-opacity", active - ? "text-muted-foreground hover:text-foreground" - : "opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-foreground", + ? "text-color4" + : "opacity-0 group-hover:opacity-100 text-color3", ])} > ✕ From cfb22028e19e69c163717db2e80e9210a85471e7 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 16:34:13 +0900 Subject: [PATCH 05/20] layout --- apps/desktop2/src/components/chat/body.tsx | 2 +- apps/desktop2/src/components/chat/header.tsx | 6 +++--- apps/desktop2/src/components/chat/input.tsx | 4 ++-- apps/desktop2/src/routes/app/main/_layout.index.tsx | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/desktop2/src/components/chat/body.tsx b/apps/desktop2/src/components/chat/body.tsx index c90047417..37c855554 100644 --- a/apps/desktop2/src/components/chat/body.tsx +++ b/apps/desktop2/src/components/chat/body.tsx @@ -37,7 +37,7 @@ export function ChatBody({ ref={scrollRef} className={cn([ "flex-1 overflow-y-auto", - chat.mode === "RightPanelOpen" && "border mt-1 mr-1 rounded-md rounded-b-none", + chat.mode === "RightPanelOpen" && "border mt-1 rounded-md rounded-b-none", ])} > {messages.length === 0 diff --git a/apps/desktop2/src/components/chat/header.tsx b/apps/desktop2/src/components/chat/header.tsx index 83e145d53..d8afe1b6b 100644 --- a/apps/desktop2/src/components/chat/header.tsx +++ b/apps/desktop2/src/components/chat/header.tsx @@ -24,8 +24,8 @@ export function ChatHeader({
@@ -102,7 +102,7 @@ function ChatGroups({ return ( - + +
+ Date: Tue, 14 Oct 2025 16:35:16 +0900 Subject: [PATCH 08/20] search --- .../src/components/main/body/search.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/desktop2/src/components/main/body/search.tsx b/apps/desktop2/src/components/main/body/search.tsx index 68664f36f..62721e5c1 100644 --- a/apps/desktop2/src/components/main/body/search.tsx +++ b/apps/desktop2/src/components/main/body/search.tsx @@ -1,5 +1,5 @@ import { Loader2Icon, SearchIcon, XIcon } from "lucide-react"; -import { useRef } from "react"; +import { useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { cn } from "@hypr/ui/lib/utils"; @@ -8,6 +8,7 @@ import { useSearch } from "../../../contexts/search/ui"; export function Search() { const { query, setQuery, isSearching, isIndexing, onFocus, onBlur } = useSearch(); const inputRef = useRef(null); + const [isFocused, setIsFocused] = useState(false); const showLoading = isSearching || isIndexing; @@ -49,9 +50,24 @@ export function Search() { { enableOnFormTags: true }, ); + const handleFocus = () => { + setIsFocused(true); + onFocus(); + }; + + const handleBlur = () => { + setIsFocused(false); + onBlur(); + }; + return ( -
-
+
+
{showLoading ? : } @@ -61,14 +77,14 @@ export function Search() { placeholder="Search anything..." value={query} onChange={(e) => setQuery(e.target.value)} - onFocus={onFocus} - onBlur={onBlur} + onFocus={handleFocus} + onBlur={handleBlur} className={cn([ "text-sm", - "w-full pl-9 py-2", + "w-full pl-9 h-full", query ? "pr-9" : "pr-4", - "rounded-lg bg-gray-100 border-0", - "focus:outline-none focus:bg-gray-200", + "rounded-lg bg-gray-100 border border-transparent", + "focus:outline-none focus:bg-gray-200 focus:border-black", ])} /> {query && ( From 2fef68e014e65cb2180cf64632c17abf9b91822f Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 16:47:18 +0900 Subject: [PATCH 09/20] coderabbit --- apps/desktop2/src/components/chat/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop2/src/components/chat/header.tsx b/apps/desktop2/src/components/chat/header.tsx index d8afe1b6b..38585bb79 100644 --- a/apps/desktop2/src/components/chat/header.tsx +++ b/apps/desktop2/src/components/chat/header.tsx @@ -102,7 +102,7 @@ function ChatGroups({ return ( - - -
- - - {tabs.map((tab) => ( - - - - ))} - - + {/* Left sidebar toggle - pinned to far left */} + {!leftsidebar.expanded && ( +
+ leftsidebar.setExpanded(true)} + />
+ )} + + {/* Navigation arrows - pinned to left */} +
+ + +
- + {/* Scrollable tabs area */} +
+ + {tabs.map((tab) => ( + + + + ))} +
+ + + + {/* Search - pinned to far right */} +
); } From 914605ca27a9548ce629cfa8a615b3a0da342680 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 18:23:11 +0900 Subject: [PATCH 12/20] carve out scroll tab function --- .../src/components/main/body/index.tsx | 18 ++++++++--------- apps/desktop2/src/utils/tabs-scroll.ts | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 apps/desktop2/src/utils/tabs-scroll.ts diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index db930e2f4..0ec7c7a89 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -1,12 +1,13 @@ import { useRouteContext } from "@tanstack/react-router"; import { ArrowLeftIcon, ArrowRightIcon, PanelLeftOpenIcon, PlusIcon } from "lucide-react"; import { Reorder } from "motion/react"; -import { useCallback, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { cn } from "@hypr/ui/lib/utils"; import { useShell } from "../../../contexts/shell"; import { type Tab, uniqueIdfromTab, useTabs } from "../../../store/zustand/tabs"; import { id } from "../../../utils"; +import { scrollTabsToEnd, setTabsScrollContainer } from "../../../utils/tabs-scroll"; import { ChatFloatingButton } from "../../chat"; import { TabContentCalendar, TabItemCalendar } from "./calendars"; import { TabContentContact, TabItemContact } from "./contacts"; @@ -43,6 +44,11 @@ function Header({ tabs }: { tabs: Tab[] }) { const { select, close, reorder, openNew } = useTabs(); const tabsScrollContainerRef = useRef(null); + useEffect(() => { + setTabsScrollContainer(tabsScrollContainerRef.current); + return () => setTabsScrollContainer(null); + }, []); + const handleNewNote = useCallback(() => { const sessionId = id(); const user_id = internalStore?.getValue("user_id"); @@ -55,15 +61,7 @@ function Header({ tabs }: { tabs: Tab[] }) { state: { editor: "raw" }, }); - // Scroll to the end to show the newly created tab - setTimeout(() => { - if (tabsScrollContainerRef.current) { - tabsScrollContainerRef.current.scrollTo({ - left: tabsScrollContainerRef.current.scrollWidth, - behavior: "smooth", - }); - } - }, 0); + scrollTabsToEnd(); }, [persistedStore, internalStore, openNew]); return ( diff --git a/apps/desktop2/src/utils/tabs-scroll.ts b/apps/desktop2/src/utils/tabs-scroll.ts new file mode 100644 index 000000000..f6e70a443 --- /dev/null +++ b/apps/desktop2/src/utils/tabs-scroll.ts @@ -0,0 +1,20 @@ +/** + * Utility to manage tabs scroll container reference and scrolling + */ + +let scrollContainerRef: HTMLDivElement | null = null; + +export function setTabsScrollContainer(ref: HTMLDivElement | null) { + scrollContainerRef = ref; +} + +export function scrollTabsToEnd() { + setTimeout(() => { + if (scrollContainerRef) { + scrollContainerRef.scrollTo({ + left: scrollContainerRef.scrollWidth, + behavior: "smooth", + }); + } + }, 0); +} From d5c833293a5ae7a4e1d5ddcb4d04fa8422d42a97 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 18:22:08 +0900 Subject: [PATCH 13/20] add scroll tab to settings menu --- apps/desktop2/src/components/main/sidebar/profile/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop2/src/components/main/sidebar/profile/index.tsx b/apps/desktop2/src/components/main/sidebar/profile/index.tsx index a3022fb82..69c41b380 100644 --- a/apps/desktop2/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop2/src/components/main/sidebar/profile/index.tsx @@ -5,6 +5,7 @@ import { useCallback, useState } from "react"; import { commands as windowsCommands } from "@hypr/plugin-windows/v1"; import { useAutoCloser } from "../../../../hooks/useAutoCloser"; import { useTabs } from "../../../../store/zustand/tabs"; +import { scrollTabsToEnd } from "../../../../utils/tabs-scroll"; import { Trial } from "./banner"; import { NotificationsItem } from "./notification"; import { UpdateChecker } from "./ota"; @@ -27,11 +28,13 @@ export function ProfileSection() { const handleClickFolders = useCallback(() => { openNew({ type: "folders", id: null, active: true }); + scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); const handleClickCalendar = useCallback(() => { openNew({ type: "calendars", month: new Date(), active: true }); + scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); @@ -44,11 +47,13 @@ export function ProfileSection() { selectedPerson: null, }, }); + scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); const handleClickDailyNote = useCallback(() => { openNew({ type: "daily", date: new Date(), active: true }); + scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); From e1221b375c927677aeca07b366e005c7661966ab Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 18:24:03 +0900 Subject: [PATCH 14/20] remove unnecessary comments --- apps/desktop2/src/components/main/body/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index 0ec7c7a89..3f0ef8f28 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -71,7 +71,6 @@ function Header({ tabs }: { tabs: Tab[] }) { !leftsidebar.expanded && "pl-[72px]", ])} > - {/* Left sidebar toggle - pinned to far left */} {!leftsidebar.expanded && (
)} - {/* Navigation arrows - pinned to left */}
- {/* Scrollable tabs area */}
- {/* Search - pinned to far right */}
); From 018f59e44f210b391445e054b47450a09335d260 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Tue, 14 Oct 2025 18:40:51 +0900 Subject: [PATCH 15/20] added cmd+w handling --- .../src/components/main/body/index.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index 3f0ef8f28..b96f38325 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -1,7 +1,9 @@ import { useRouteContext } from "@tanstack/react-router"; +import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { ArrowLeftIcon, ArrowRightIcon, PanelLeftOpenIcon, PlusIcon } from "lucide-react"; import { Reorder } from "motion/react"; import { useCallback, useEffect, useRef } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; import { cn } from "@hypr/ui/lib/utils"; import { useShell } from "../../../contexts/shell"; @@ -19,9 +21,29 @@ import { Search } from "./search"; import { TabContentNote, TabItemNote } from "./sessions"; export function Body() { - const { tabs, currentTab } = useTabs(); + const { tabs, currentTab, close } = useTabs(); const { chat } = useShell(); + useHotkeys( + "mod+w", + async (e) => { + e.preventDefault(); + + if (tabs.length > 1) { + // If more than one tab, close the current tab + if (currentTab) { + close(currentTab); + } + } else if (tabs.length === 1) { + // If only one tab left, close the window + const appWindow = getCurrentWebviewWindow(); + await appWindow.close(); + } + }, + { enableOnFormTags: true }, + [tabs, currentTab, close], + ); + if (!currentTab) { return null; } From 76adde704746b0df21e73d3a0caf55444458c2a7 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Tue, 14 Oct 2025 18:53:32 +0900 Subject: [PATCH 16/20] chores --- .../src/components/main/body/index.tsx | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index b96f38325..e5a4d7992 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -21,28 +21,10 @@ import { Search } from "./search"; import { TabContentNote, TabItemNote } from "./sessions"; export function Body() { - const { tabs, currentTab, close } = useTabs(); + const { tabs, currentTab } = useTabs(); const { chat } = useShell(); - useHotkeys( - "mod+w", - async (e) => { - e.preventDefault(); - - if (tabs.length > 1) { - // If more than one tab, close the current tab - if (currentTab) { - close(currentTab); - } - } else if (tabs.length === 1) { - // If only one tab left, close the window - const appWindow = getCurrentWebviewWindow(); - await appWindow.close(); - } - }, - { enableOnFormTags: true }, - [tabs, currentTab, close], - ); + useTabCloseHotkey(); if (!currentTab) { return null; @@ -237,3 +219,23 @@ function Content({ tab }: { tab: Tab }) { return null; } + +const useTabCloseHotkey = () => { + const { tabs, currentTab, close } = useTabs(); + + useHotkeys( + "mod+w", + async (e) => { + e.preventDefault(); + + if (currentTab && tabs.length > 1) { + close(currentTab); + } else { + const appWindow = getCurrentWebviewWindow(); + await appWindow.close(); + } + }, + { enableOnFormTags: true }, + [tabs, currentTab, close], + ); +}; From 6e1f25f0138ae9683a80e163c4e575094ca9788c Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Tue, 14 Oct 2025 19:03:47 +0900 Subject: [PATCH 17/20] add useScrollActiveTabIntoView --- .../src/components/main/body/index.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index e5a4d7992..81b906374 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -47,6 +47,7 @@ function Header({ tabs }: { tabs: Tab[] }) { const { leftsidebar } = useShell(); const { select, close, reorder, openNew } = useTabs(); const tabsScrollContainerRef = useRef(null); + const setTabRef = useScrollActiveTabIntoView(tabs); useEffect(() => { setTabsScrollContainer(tabsScrollContainerRef.current); @@ -134,6 +135,7 @@ function Header({ tabs }: { tabs: Tab[] }) { key={uniqueIdfromTab(tab)} value={tab} as="div" + ref={(el) => setTabRef(tab, el)} style={{ position: "relative" }} className="h-full z-10" layoutScroll @@ -239,3 +241,32 @@ const useTabCloseHotkey = () => { [tabs, currentTab, close], ); }; + +function useScrollActiveTabIntoView(tabs: Tab[]) { + const tabRefsMap = useRef>(new Map()); + + useEffect(() => { + const activeTab = tabs.find((tab) => tab.active); + if (activeTab) { + const tabKey = uniqueIdfromTab(activeTab); + const tabElement = tabRefsMap.current.get(tabKey); + if (tabElement) { + tabElement.scrollIntoView({ + behavior: "smooth", + inline: "nearest", + block: "nearest", + }); + } + } + }, [tabs]); + + const setTabRef = useCallback((tab: Tab, el: HTMLDivElement | null) => { + if (el) { + tabRefsMap.current.set(uniqueIdfromTab(tab), el); + } else { + tabRefsMap.current.delete(uniqueIdfromTab(tab)); + } + }, []); + + return setTabRef; +} From aeceac01e14d9243ead478841ed4ee7a2008a72a Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Tue, 14 Oct 2025 19:12:36 +0900 Subject: [PATCH 18/20] scrollTabsToEnd is not needed because of useScrollActiveTabIntoView --- .../src/components/main/body/index.tsx | 8 -------- apps/desktop2/src/utils/tabs-scroll.ts | 20 ------------------- 2 files changed, 28 deletions(-) delete mode 100644 apps/desktop2/src/utils/tabs-scroll.ts diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index 81b906374..85858195f 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -9,7 +9,6 @@ import { cn } from "@hypr/ui/lib/utils"; import { useShell } from "../../../contexts/shell"; import { type Tab, uniqueIdfromTab, useTabs } from "../../../store/zustand/tabs"; import { id } from "../../../utils"; -import { scrollTabsToEnd, setTabsScrollContainer } from "../../../utils/tabs-scroll"; import { ChatFloatingButton } from "../../chat"; import { TabContentCalendar, TabItemCalendar } from "./calendars"; import { TabContentContact, TabItemContact } from "./contacts"; @@ -49,11 +48,6 @@ function Header({ tabs }: { tabs: Tab[] }) { const tabsScrollContainerRef = useRef(null); const setTabRef = useScrollActiveTabIntoView(tabs); - useEffect(() => { - setTabsScrollContainer(tabsScrollContainerRef.current); - return () => setTabsScrollContainer(null); - }, []); - const handleNewNote = useCallback(() => { const sessionId = id(); const user_id = internalStore?.getValue("user_id"); @@ -65,8 +59,6 @@ function Header({ tabs }: { tabs: Tab[] }) { active: true, state: { editor: "raw" }, }); - - scrollTabsToEnd(); }, [persistedStore, internalStore, openNew]); return ( diff --git a/apps/desktop2/src/utils/tabs-scroll.ts b/apps/desktop2/src/utils/tabs-scroll.ts deleted file mode 100644 index f6e70a443..000000000 --- a/apps/desktop2/src/utils/tabs-scroll.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Utility to manage tabs scroll container reference and scrolling - */ - -let scrollContainerRef: HTMLDivElement | null = null; - -export function setTabsScrollContainer(ref: HTMLDivElement | null) { - scrollContainerRef = ref; -} - -export function scrollTabsToEnd() { - setTimeout(() => { - if (scrollContainerRef) { - scrollContainerRef.scrollTo({ - left: scrollContainerRef.scrollWidth, - behavior: "smooth", - }); - } - }, 0); -} From 36c2db7ca836af11dfaa754d7d5c8cf40d4a29c7 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Tue, 14 Oct 2025 19:28:03 +0900 Subject: [PATCH 19/20] i forgot to remove this --- apps/desktop2/src/components/main/sidebar/profile/index.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/desktop2/src/components/main/sidebar/profile/index.tsx b/apps/desktop2/src/components/main/sidebar/profile/index.tsx index 69c41b380..a3022fb82 100644 --- a/apps/desktop2/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop2/src/components/main/sidebar/profile/index.tsx @@ -5,7 +5,6 @@ import { useCallback, useState } from "react"; import { commands as windowsCommands } from "@hypr/plugin-windows/v1"; import { useAutoCloser } from "../../../../hooks/useAutoCloser"; import { useTabs } from "../../../../store/zustand/tabs"; -import { scrollTabsToEnd } from "../../../../utils/tabs-scroll"; import { Trial } from "./banner"; import { NotificationsItem } from "./notification"; import { UpdateChecker } from "./ota"; @@ -28,13 +27,11 @@ export function ProfileSection() { const handleClickFolders = useCallback(() => { openNew({ type: "folders", id: null, active: true }); - scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); const handleClickCalendar = useCallback(() => { openNew({ type: "calendars", month: new Date(), active: true }); - scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); @@ -47,13 +44,11 @@ export function ProfileSection() { selectedPerson: null, }, }); - scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); const handleClickDailyNote = useCallback(() => { openNew({ type: "daily", date: new Date(), active: true }); - scrollTabsToEnd(); closeMenu(); }, [openNew, closeMenu]); From bbaf4e12e3742b16e4a2c9f04c8a841b64b6c8c4 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Tue, 14 Oct 2025 19:54:18 +0900 Subject: [PATCH 20/20] done --- .../src/components/main/body/index.tsx | 30 ++- apps/desktop2/src/store/zustand/tabs.test.ts | 151 +++++++++++++ apps/desktop2/src/store/zustand/tabs.ts | 200 +++++++++++++++--- 3 files changed, 349 insertions(+), 32 deletions(-) create mode 100644 apps/desktop2/src/store/zustand/tabs.test.ts diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx index 85858195f..70d428a2c 100644 --- a/apps/desktop2/src/components/main/body/index.tsx +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -44,7 +44,7 @@ function Header({ tabs }: { tabs: Tab[] }) { const { persistedStore, internalStore } = useRouteContext({ from: "__root__" }); const { leftsidebar } = useShell(); - const { select, close, reorder, openNew } = useTabs(); + const { select, close, reorder, openNew, goBack, goNext, canGoBack, canGoNext } = useTabs(); const tabsScrollContainerRef = useRef(null); const setTabRef = useScrollActiveTabIntoView(tabs); @@ -79,30 +79,46 @@ function Header({ tabs }: { tabs: Tab[] }) {
diff --git a/apps/desktop2/src/store/zustand/tabs.test.ts b/apps/desktop2/src/store/zustand/tabs.test.ts new file mode 100644 index 000000000..517a6b1e7 --- /dev/null +++ b/apps/desktop2/src/store/zustand/tabs.test.ts @@ -0,0 +1,151 @@ +import { beforeEach, describe, expect, test } from "vitest"; +import { type Tab, useTabs } from "./tabs"; + +describe("Tab History Navigation", () => { + beforeEach(() => { + useTabs.setState({ + currentTab: null, + tabs: [], + history: new Map(), + canGoBack: false, + canGoNext: false, + }); + }); + + test("basic navigation: open tab1 -> open tab2 -> goBack -> goNext", () => { + const tab1: Tab = { + type: "sessions", + id: "session-1", + active: true, + state: { editor: "raw" }, + }; + const tab2: Tab = { + type: "sessions", + id: "session-2", + active: true, + state: { editor: "raw" }, + }; + + useTabs.getState().openCurrent(tab1); + let state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-1" }); + expect(state.canGoBack).toBe(false); + expect(state.canGoNext).toBe(false); + + useTabs.getState().openCurrent(tab2); + state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-2" }); + expect(state.canGoBack).toBe(true); + expect(state.canGoNext).toBe(false); + + useTabs.getState().goBack(); + state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-1" }); + expect(state.canGoBack).toBe(false); + expect(state.canGoNext).toBe(true); + + useTabs.getState().goNext(); + state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-2" }); + expect(state.canGoBack).toBe(true); + expect(state.canGoNext).toBe(false); + }); + + test("truncate forward history: tab1 -> tab2 -> goBack -> tab3", () => { + const tab1: Tab = { + type: "sessions", + id: "session-1", + active: true, + state: { editor: "raw" }, + }; + const tab2: Tab = { + type: "sessions", + id: "session-2", + active: true, + state: { editor: "raw" }, + }; + const tab3: Tab = { + type: "sessions", + id: "session-3", + active: true, + state: { editor: "raw" }, + }; + + useTabs.getState().openCurrent(tab1); + useTabs.getState().openCurrent(tab2); + useTabs.getState().goBack(); + + let state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-1" }); + expect(state.canGoNext).toBe(true); + + useTabs.getState().openCurrent(tab3); + + state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-3" }); + expect(state.canGoBack).toBe(true); + expect(state.canGoNext).toBe(false); + }); + + test("boundary: cannot go back at start", () => { + const tab1: Tab = { + type: "sessions", + id: "session-1", + active: true, + state: { editor: "raw" }, + }; + + useTabs.getState().openCurrent(tab1); + useTabs.getState().goBack(); + + const state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-1" }); + expect(state.canGoBack).toBe(false); + }); + + test("boundary: cannot go forward at end", () => { + const tab1: Tab = { + type: "sessions", + id: "session-1", + active: true, + state: { editor: "raw" }, + }; + const tab2: Tab = { + type: "sessions", + id: "session-2", + active: true, + state: { editor: "raw" }, + }; + + useTabs.getState().openCurrent(tab1); + useTabs.getState().openCurrent(tab2); + useTabs.getState().goNext(); + + const state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "session-2" }); + expect(state.canGoNext).toBe(false); + }); + + test("openCurrent with active:false in input should still track history", () => { + useTabs.getState().openCurrent({ + type: "sessions", + id: "tab-1", + active: false, + state: { editor: "raw" }, + }); + expect(useTabs.getState().canGoBack).toBe(false); + + useTabs.getState().openCurrent({ + type: "sessions", + id: "tab-2", + active: false, + state: { editor: "raw" }, + }); + expect(useTabs.getState().canGoBack).toBe(true); + + useTabs.getState().goBack(); + const state = useTabs.getState(); + expect(state.currentTab).toMatchObject({ id: "tab-1" }); + expect(state.canGoNext).toBe(true); + }); +}); diff --git a/apps/desktop2/src/store/zustand/tabs.ts b/apps/desktop2/src/store/zustand/tabs.ts index a54a2affb..2523fce27 100644 --- a/apps/desktop2/src/store/zustand/tabs.ts +++ b/apps/desktop2/src/store/zustand/tabs.ts @@ -3,14 +3,23 @@ import { create } from "zustand"; import { TABLES } from "../tinybase/persisted"; +type TabHistory = { + stack: Tab[]; + currentIndex: number; +}; + type State = { currentTab: Tab | null; tabs: Tab[]; + history: Map; + canGoBack: boolean; + canGoNext: boolean; }; type Actions = & TabUpdater - & TabStateUpdater; + & TabStateUpdater + & TabNavigator; type TabUpdater = { setTabs: (tabs: Tab[]) => void; @@ -26,59 +35,136 @@ type TabStateUpdater = { updateSessionTabState: (tab: Tab, state: Extract["state"]) => void; }; +type TabNavigator = { + goBack: () => void; + goNext: () => void; +}; + type Store = State & Actions; +const ACTIVE_TAB_SLOT_ID = "active-tab-history"; + +const getSlotId = (tab: Tab): string => { + return tab.active ? ACTIVE_TAB_SLOT_ID : `inactive-${uniqueIdfromTab(tab)}`; +}; + +const computeHistoryFlags = ( + history: Map, + currentTab: Tab | null, +): { canGoBack: boolean; canGoNext: boolean } => { + if (!currentTab) { + return { canGoBack: false, canGoNext: false }; + } + const slotId = getSlotId(currentTab); + const tabHistory = history.get(slotId); + if (!tabHistory) { + return { canGoBack: false, canGoNext: false }; + } + return { + canGoBack: tabHistory.currentIndex > 0, + canGoNext: tabHistory.currentIndex < tabHistory.stack.length - 1, + }; +}; + +const pushHistory = (history: Map, tab: Tab): Map => { + const newHistory = new Map(history); + const slotId = getSlotId(tab); + const existing = newHistory.get(slotId); + + if (existing) { + const newStack = existing.stack.slice(0, existing.currentIndex + 1); + newStack.push(tab); + newHistory.set(slotId, { stack: newStack, currentIndex: newStack.length - 1 }); + } else { + newHistory.set(slotId, { stack: [tab], currentIndex: 0 }); + } + + return newHistory; +}; + +const updateHistoryCurrent = (history: Map, tab: Tab): Map => { + const newHistory = new Map(history); + const slotId = getSlotId(tab); + const existing = newHistory.get(slotId); + + if (existing && existing.currentIndex >= 0) { + const newStack = [...existing.stack]; + newStack[existing.currentIndex] = tab; + newHistory.set(slotId, { ...existing, stack: newStack }); + } + + return newHistory; +}; + export const useTabs = create((set, get, _store) => ({ currentTab: null, tabs: [], + history: new Map(), + canGoBack: false, + canGoNext: false, setTabs: (tabs) => { const tabsWithDefaults = tabs.map(t => tabSchema.parse(t)); - set({ tabs: tabsWithDefaults, currentTab: tabsWithDefaults.find((t) => t.active) || null }); + const currentTab = tabsWithDefaults.find((t) => t.active) || null; + const history = new Map(); + + tabsWithDefaults.forEach((tab) => { + if (tab.active) { + history.set(getSlotId(tab), { stack: [tab], currentIndex: 0 }); + } + }); + + const flags = computeHistoryFlags(history, currentTab); + set({ tabs: tabsWithDefaults, currentTab, history, ...flags }); }, openCurrent: (newTab) => { - const { tabs } = get(); + const { tabs, history } = get(); const tabWithDefaults = tabSchema.parse(newTab); + const activeTab = { ...tabWithDefaults, active: true }; const existingTabIdx = tabs.findIndex((t) => t.active); - if (existingTabIdx === -1) { - const nextTabs = tabs + const nextTabs = existingTabIdx === -1 + ? tabs .filter((t) => !isSameTab(t, tabWithDefaults)) .map((t) => ({ ...t, active: false })) - .concat([{ ...tabWithDefaults, active: true }]); - set({ tabs: nextTabs, currentTab: tabWithDefaults }); - } else { - const nextTabs = tabs + .concat([activeTab]) + : tabs .map((t, idx) => idx === existingTabIdx - ? { ...tabWithDefaults, active: true } + ? activeTab : isSameTab(t, tabWithDefaults) ? null : { ...t, active: false } ) .filter((t): t is Tab => t !== null); - set({ tabs: nextTabs, currentTab: tabWithDefaults }); - } + + const nextHistory = pushHistory(history, activeTab); + const flags = computeHistoryFlags(nextHistory, activeTab); + set({ tabs: nextTabs, currentTab: activeTab, history: nextHistory, ...flags }); }, openNew: (tab) => { - const { tabs } = get(); + const { tabs, history } = get(); const tabWithDefaults = tabSchema.parse(tab); + const activeTab = { ...tabWithDefaults, active: true }; const nextTabs = tabs .filter((t) => !isSameTab(t, tabWithDefaults)) .map((t) => ({ ...t, active: false })) - .concat([{ ...tabWithDefaults, active: true }]); - set({ tabs: nextTabs, currentTab: tabWithDefaults }); + .concat([activeTab]); + const nextHistory = pushHistory(history, activeTab); + const flags = computeHistoryFlags(nextHistory, activeTab); + set({ tabs: nextTabs, currentTab: activeTab, history: nextHistory, ...flags }); }, select: (tab) => { - const { tabs } = get(); + const { tabs, history } = get(); const nextTabs = tabs.map((t) => ({ ...t, active: isSameTab(t, tab) })); - set({ tabs: nextTabs, currentTab: tab }); + const flags = computeHistoryFlags(history, tab); + set({ tabs: nextTabs, currentTab: tab, ...flags }); }, close: (tab) => { - const { tabs } = get(); + const { tabs, history } = get(); const remainingTabs = tabs.filter((t) => !isSameTab(t, tab)); if (remainingTabs.length === 0) { - return set({ tabs: [], currentTab: null }); + return set({ tabs: [], currentTab: null, canGoBack: false, canGoNext: false }); } const closedTabIndex = tabs.findIndex((t) => isSameTab(t, tab)); @@ -87,14 +173,22 @@ export const useTabs = create((set, get, _store) => ({ : remainingTabs.length - 1; const nextTabs = remainingTabs.map((t, idx) => ({ ...t, active: idx === nextActiveIndex })); - set({ tabs: nextTabs, currentTab: nextTabs[nextActiveIndex] }); + const nextCurrentTab = nextTabs[nextActiveIndex]; + + const nextHistory = new Map(history); + nextHistory.delete(getSlotId(tab)); + + const flags = computeHistoryFlags(nextHistory, nextCurrentTab); + set({ tabs: nextTabs, currentTab: nextCurrentTab, history: nextHistory, ...flags }); }, reorder: (tabs) => { + const { history } = get(); const currentTab = tabs.find((t) => t.active) || null; - set({ tabs, currentTab }); + const flags = computeHistoryFlags(history, currentTab); + set({ tabs, currentTab, ...flags }); }, updateSessionTabState: (tab, state) => { - const { tabs, currentTab } = get(); + const { tabs, currentTab, history } = get(); const nextTabs = tabs.map((t) => isSameTab(t, tab) && t.type === "sessions" ? { ...t, state } @@ -103,10 +197,15 @@ export const useTabs = create((set, get, _store) => ({ const nextCurrentTab = currentTab && isSameTab(currentTab, tab) && currentTab.type === "sessions" ? { ...currentTab, state } : currentTab; - set({ tabs: nextTabs, currentTab: nextCurrentTab }); + + const nextHistory = nextCurrentTab && isSameTab(nextCurrentTab, tab) + ? updateHistoryCurrent(history, nextCurrentTab) + : history; + + set({ tabs: nextTabs, currentTab: nextCurrentTab, history: nextHistory }); }, updateContactsTabState: (tab, state) => { - const { tabs, currentTab } = get(); + const { tabs, currentTab, history } = get(); const nextTabs = tabs.map((t) => isSameTab(t, tab) && t.type === "contacts" ? { ...t, state } @@ -116,7 +215,58 @@ export const useTabs = create((set, get, _store) => ({ const nextCurrentTab = currentTab && isSameTab(currentTab, tab) && currentTab.type === "contacts" ? { ...currentTab, state } : currentTab; - set({ tabs: nextTabs, currentTab: nextCurrentTab }); + + const nextHistory = nextCurrentTab && isSameTab(nextCurrentTab, tab) + ? updateHistoryCurrent(history, nextCurrentTab) + : history; + + set({ tabs: nextTabs, currentTab: nextCurrentTab, history: nextHistory }); + }, + goBack: () => { + const { tabs, history, currentTab } = get(); + if (!currentTab) { + return; + } + + const slotId = getSlotId(currentTab); + const tabHistory = history.get(slotId); + if (!tabHistory || tabHistory.currentIndex === 0) { + return; + } + + const prevIndex = tabHistory.currentIndex - 1; + const prevTab = tabHistory.stack[prevIndex]; + + const nextTabs = tabs.map((t) => t.active ? prevTab : t); + + const nextHistory = new Map(history); + nextHistory.set(slotId, { ...tabHistory, currentIndex: prevIndex }); + + const flags = computeHistoryFlags(nextHistory, prevTab); + set({ tabs: nextTabs, currentTab: prevTab, history: nextHistory, ...flags }); + }, + goNext: () => { + const { tabs, history, currentTab } = get(); + if (!currentTab) { + return; + } + + const slotId = getSlotId(currentTab); + const tabHistory = history.get(slotId); + if (!tabHistory || tabHistory.currentIndex >= tabHistory.stack.length - 1) { + return; + } + + const nextIndex = tabHistory.currentIndex + 1; + const nextTab = tabHistory.stack[nextIndex]; + + const nextTabs = tabs.map((t) => t.active ? nextTab : t); + + const nextHistory = new Map(history); + nextHistory.set(slotId, { ...tabHistory, currentIndex: nextIndex }); + + const flags = computeHistoryFlags(nextHistory, nextTab); + set({ tabs: nextTabs, currentTab: nextTab, history: nextHistory, ...flags }); }, }));