diff --git a/.gitignore b/.gitignore index f292602b41..56fb6cdf84 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ supabase/.seed/ *storybook.log .cache/ +.dprint-cache/ internal .turbo diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1fe71e8eee..29858a9065 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -96,6 +96,7 @@ "@sentry/react": "^8.55.0", "@supabase/supabase-js": "^2.95.3", "@t3-oss/env-core": "^0.13.10", + "@tailwindcss/postcss": "^4.1.18", "@tanstack/react-form": "^1.28.0", "@tanstack/react-query": "^5.90.20", "@tanstack/react-router": "^1.159.5", diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs index feee9de32d..158133d366 100644 --- a/apps/desktop/src-tauri/build.rs +++ b/apps/desktop/src-tauri/build.rs @@ -1,6 +1,70 @@ +fn ensure_permission_files_exist() { + // Handle missing permission files referenced by DEP_* env vars + for (key, value) in std::env::vars_os() { + let key_str = key.to_string_lossy(); + if key_str.starts_with("DEP_") + && (key_str.ends_with("_PERMISSION_FILES_PATH") + || key_str.ends_with("_GLOBAL_SCOPE_SCHEMA_PATH")) + { + let path = std::path::PathBuf::from(&value); + if !path.exists() { + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if key_str.ends_with("_PERMISSION_FILES_PATH") { + let _ = std::fs::write(&path, "[]"); + } else { + let _ = std::fs::write(&path, "{}"); + } + } + } + } + + // Also scan the build directory for stale tauri outputs with missing permission files. + // This handles cases where folder renames invalidate cached build artifacts. + let out_dir = match std::env::var("OUT_DIR") { + Ok(d) => std::path::PathBuf::from(d), + Err(_) => return, + }; + let build_dir = match out_dir.parent().and_then(|p| p.parent()) { + Some(d) => d.to_path_buf(), + None => return, + }; + let entries = match std::fs::read_dir(&build_dir) { + Ok(e) => e, + Err(_) => return, + }; + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if !name_str.starts_with("tauri-") || name_str.starts_with("tauri-build-") { + continue; + } + let out_subdir = entry.path().join("out"); + if !out_subdir.is_dir() { + continue; + } + // Find expected permission files by checking if the out dir is nearly empty + let has_permission_file = std::fs::read_dir(&out_subdir) + .map(|rd| { + rd.flatten().any(|e| { + e.file_name() + .to_string_lossy() + .ends_with("permission-files") + }) + }) + .unwrap_or(false); + if !has_permission_file { + // Remove stale build directory to force cargo to re-run the build script + let _ = std::fs::remove_dir_all(entry.path()); + } + } +} + fn main() { #[cfg(target_os = "macos")] println!("cargo:rustc-link-arg=-fapple-link-rtlib"); + ensure_permission_files_exist(); tauri_build::build() } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index e12ed2849e..e55f3c09ea 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -8,18 +8,6 @@ "permissions": [ "core:default", "core:window:default", - "core:window:allow-start-dragging", - "core:window:allow-set-fullscreen", - "core:window:allow-minimize", - "core:window:allow-maximize", - "core:window:allow-close", - "core:window:allow-show", - "core:window:allow-center", - "core:window:allow-set-size", - "core:window:allow-set-focus", - "core:window:allow-set-position", - "core:window:allow-is-maximized", - "core:window:allow-toggle-maximize", "apple-calendar:default", "apple-contact:default", "audio-priority:default", diff --git a/apps/desktop/src/components/main/body/header-listen-button.tsx b/apps/desktop/src/components/main/body/header-listen-button.tsx new file mode 100644 index 0000000000..6e8a727a9b --- /dev/null +++ b/apps/desktop/src/components/main/body/header-listen-button.tsx @@ -0,0 +1,511 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useRouteContext } from "@tanstack/react-router"; +import { downloadDir } from "@tauri-apps/api/path"; +import { open as selectFile } from "@tauri-apps/plugin-dialog"; +import { Effect, pipe } from "effect"; +import { ChevronDown } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import { commands as analyticsCommands } from "@hypr/plugin-analytics"; +import { commands as fsSyncCommands } from "@hypr/plugin-fs-sync"; +import { commands as listener2Commands } from "@hypr/plugin-listener2"; +import { md2json } from "@hypr/tiptap/shared"; +import { Button } from "@hypr/ui/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@hypr/ui/components/ui/popover"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@hypr/ui/components/ui/tooltip"; +import { cn } from "@hypr/utils"; + +import { useAITask } from "../../../contexts/ai-task"; +import { useListener } from "../../../contexts/listener"; +import { fromResult } from "../../../effect"; +import { getEligibility } from "../../../hooks/autoEnhance/eligibility"; +import { useCreateEnhancedNote } from "../../../hooks/useEnhancedNotes"; +import { useLanguageModel } from "../../../hooks/useLLMConnection"; +import { useRunBatch } from "../../../hooks/useRunBatch"; +import * as main from "../../../store/tinybase/store/main"; +import * as settings from "../../../store/tinybase/store/settings"; +import { createTaskId } from "../../../store/zustand/ai-task/task-configs"; +import { type Tab, useTabs } from "../../../store/zustand/tabs"; +import { id } from "../../../utils"; +import { ChannelProfile } from "../../../utils/segment"; +import { useNewNoteAndListen } from "../shared"; +import { RecordingIcon } from "./sessions/shared"; + +type UploadKind = "audio" | "transcript"; +type UploadState = { kind: UploadKind; sessionId: string } | null; + +export function HeaderListenButton({ + isListening, + listeningTab, + hasOverflow, +}: { + isListening: boolean; + listeningTab: Tab | null | undefined; + hasOverflow: boolean; +}) { + const handleNewNoteAndListen = useNewNoteAndListen(); + const select = useTabs((state) => state.select); + const stop = useListener((state) => state.stop); + const finalizing = useListener((state) => state.live.status === "finalizing"); + + const handleStopAndFocus = useCallback(() => { + if (listeningTab) { + select(listeningTab); + } + stop(); + }, [listeningTab, select, stop]); + + if (isListening && listeningTab) { + return ( + + ); + } + + return ( + + ); +} + +function HeaderStopButton({ + compact, + finalizing, + onStop, +}: { + compact: boolean; + finalizing: boolean; + onStop: () => void; +}) { + return ( + + + + + + {finalizing ? "Finalizing..." : "Stop listening"} + + + ); +} + +function HeaderStartButton({ + compact, + onStart, +}: { + compact: boolean; + onStart: () => void; +}) { + const [uploadState, setUploadState] = useState(null); + const [popoverOpen, setPopoverOpen] = useState(false); + + const { persistedStore, internalStore } = useRouteContext({ + from: "__root__", + }); + const openNew = useTabs((state) => state.openNew); + + const createNoteAndUpload = useCallback( + (kind: UploadKind) => { + setPopoverOpen(false); + const user_id = internalStore?.getValue("user_id"); + const sessionId = id(); + persistedStore?.setRow("sessions", sessionId, { + user_id, + created_at: new Date().toISOString(), + title: "", + }); + openNew({ type: "sessions", id: sessionId }); + setUploadState({ kind, sessionId }); + }, + [persistedStore, internalStore, openNew], + ); + + return ( + <> + {uploadState && ( + setUploadState(null)} + /> + )} +
+ + + + + + +
+ + +
+
+
+
+ + ); +} + +function UploadWorker({ + sessionId, + kind, + onDone, +}: { + sessionId: string; + kind: UploadKind; + onDone: () => void; +}) { + const queryClient = useQueryClient(); + const handleBatchStarted = useListener((state) => state.handleBatchStarted); + const handleBatchFailed = useListener((state) => state.handleBatchFailed); + const clearBatchSession = useListener((state) => state.clearBatchSession); + + const store = main.UI.useStore(main.STORE_ID) as main.Store | undefined; + const indexes = main.UI.useIndexes(main.STORE_ID); + const { user_id } = main.UI.useValues(main.STORE_ID); + + const updateSessionTabState = useTabs((state) => state.updateSessionTabState); + const createEnhancedNote = useCreateEnhancedNote(); + const model = useLanguageModel("enhance"); + const generate = useAITask((state) => state.generate); + const selectedTemplateId = settings.UI.useValue( + "selected_template_id", + settings.STORE_ID, + ) as string | undefined; + + const sessionTab = useTabs((state) => { + const found = state.tabs.find( + (tab): tab is Extract => + tab.type === "sessions" && tab.id === sessionId, + ); + return found ?? null; + }); + + const runBatch = useRunBatch(sessionId); + + const sessionTabRef = useRef(sessionTab); + sessionTabRef.current = sessionTab; + + const runBatchRef = useRef(runBatch); + runBatchRef.current = runBatch; + + const onDoneRef = useRef(onDone); + onDoneRef.current = onDone; + + const storeRef = useRef(store); + storeRef.current = store; + + const triggerEnhance = useCallback(() => { + if (!storeRef.current || !indexes || !model) { + return; + } + + const transcriptIds = indexes.getSliceRowIds( + main.INDEXES.transcriptBySession, + sessionId, + ); + const hasTranscript = transcriptIds.length > 0; + const eligibility = getEligibility( + hasTranscript, + transcriptIds, + storeRef.current, + ); + + if (!eligibility.eligible) { + return; + } + + const templateId = selectedTemplateId || undefined; + const enhancedNoteId = createEnhancedNote(sessionId, templateId); + if (!enhancedNoteId) { + return; + } + + const tab = sessionTabRef.current; + if (tab) { + updateSessionTabState(tab, { + ...tab.state, + view: { type: "enhanced", id: enhancedNoteId }, + }); + } + + const enhanceTaskId = createTaskId(enhancedNoteId, "enhance"); + void generate(enhanceTaskId, { + model, + taskType: "enhance", + args: { sessionId, enhancedNoteId, templateId }, + onComplete: (text) => { + if (!text || !storeRef.current) { + return; + } + try { + const jsonContent = md2json(text); + storeRef.current.setPartialRow("enhanced_notes", enhancedNoteId, { + content: JSON.stringify(jsonContent), + }); + const currentTitle = storeRef.current.getCell( + "sessions", + sessionId, + "title", + ); + const trimmedTitle = + typeof currentTitle === "string" ? currentTitle.trim() : ""; + if (!trimmedTitle && model) { + const titleTaskId = createTaskId(sessionId, "title"); + void generate(titleTaskId, { + model, + taskType: "title", + args: { sessionId }, + onComplete: (titleText) => { + if (titleText && storeRef.current) { + const trimmed = titleText.trim(); + if (trimmed && trimmed !== "") { + storeRef.current.setPartialRow("sessions", sessionId, { + title: trimmed, + }); + } + } + }, + }); + } + } catch (error) { + console.error("Failed to convert markdown to JSON:", error); + } + }, + }); + }, [ + indexes, + model, + sessionId, + createEnhancedNote, + selectedTemplateId, + updateSessionTabState, + generate, + ]); + + useEffect(() => { + const program = pipe( + Effect.promise(() => downloadDir()), + Effect.flatMap((defaultPath) => + Effect.promise(() => + selectFile({ + title: kind === "audio" ? "Upload Audio" : "Upload Transcript", + multiple: false, + directory: false, + defaultPath, + filters: + kind === "audio" + ? [ + { + name: "Audio", + extensions: ["wav", "mp3", "ogg", "mp4", "m4a", "flac"], + }, + ] + : [{ name: "Transcript", extensions: ["vtt", "srt"] }], + }), + ), + ), + Effect.tap(() => Effect.sync(() => onDoneRef.current())), + Effect.flatMap((selection) => { + if (!selection) { + return Effect.void; + } + const path = Array.isArray(selection) ? selection[0] : selection; + if (!path) { + return Effect.void; + } + + if (kind === "transcript") { + return pipe( + fromResult(listener2Commands.parseSubtitle(path)), + Effect.tap((subtitle) => + Effect.sync(() => { + const store = storeRef.current; + if (!store || subtitle.tokens.length === 0) { + return; + } + const tab = sessionTabRef.current; + if (tab) { + updateSessionTabState(tab, { + ...tab.state, + view: { type: "transcript" }, + }); + } + const transcriptId = crypto.randomUUID(); + const createdAt = new Date().toISOString(); + const words = subtitle.tokens.map((token) => ({ + id: crypto.randomUUID(), + transcript_id: transcriptId, + text: token.text, + start_ms: token.start_time, + end_ms: token.end_time, + channel: ChannelProfile.MixedCapture, + user_id: user_id ?? "", + created_at: new Date().toISOString(), + })); + store.setRow("transcripts", transcriptId, { + session_id: sessionId, + user_id: user_id ?? "", + created_at: createdAt, + started_at: Date.now(), + words: JSON.stringify(words), + speaker_hints: "[]", + }); + void analyticsCommands.event({ + event: "file_uploaded", + file_type: "transcript", + token_count: subtitle.tokens.length, + }); + triggerEnhance(); + }), + ), + ); + } + + return pipe( + Effect.sync(() => { + const tab = sessionTabRef.current; + if (tab) { + updateSessionTabState(tab, { + ...tab.state, + view: { type: "transcript" }, + }); + } + handleBatchStarted(sessionId); + }), + Effect.flatMap(() => + fromResult(fsSyncCommands.audioImport(sessionId, path)), + ), + Effect.tap(() => + Effect.sync(() => { + void analyticsCommands.event({ + event: "file_uploaded", + file_type: "audio", + }); + void queryClient.invalidateQueries({ + queryKey: ["audio", sessionId, "exist"], + }); + void queryClient.invalidateQueries({ + queryKey: ["audio", sessionId, "url"], + }); + }), + ), + Effect.tap(() => Effect.sync(() => clearBatchSession(sessionId))), + Effect.flatMap((importedPath) => + Effect.tryPromise({ + try: () => runBatchRef.current(importedPath), + catch: (error) => error, + }), + ), + Effect.tap(() => Effect.sync(() => triggerEnhance())), + Effect.catchAll((error: unknown) => + Effect.sync(() => { + const msg = + error instanceof Error ? error.message : String(error); + handleBatchFailed(sessionId, msg); + }), + ), + ); + }), + ); + + Effect.runPromise(program).catch((error) => { + console.error("[header-upload] failed:", error); + onDoneRef.current(); + }); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return null; +} diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index e055f37e29..55c2c368b2 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -50,6 +50,7 @@ import { } from "./extensions"; import { loadExtensionPanels } from "./extensions/registry"; import { TabContentFolder, TabItemFolder } from "./folders"; +import { HeaderListenButton } from "./header-listen-button"; import { TabContentHuman, TabItemHuman } from "./humans"; import { TabContentOnboarding, TabItemOnboarding } from "./onboarding"; import { TabContentNote, TabItemNote } from "./sessions"; @@ -134,23 +135,30 @@ function Header({ tabs }: { tabs: Tab[] }) { : null, [isListening, liveSessionId, tabs], ); - const regularTabs = useMemo( - () => - listeningTab - ? tabs.filter((t) => !(t.type === "sessions" && t.id === liveSessionId)) - : tabs, - [listeningTab, tabs, liveSessionId], - ); const tabsScrollContainerRef = useRef(null); const handleNewEmptyTab = useNewEmptyTab(); - const handleNewNoteAndListen = useNewNoteAndListen(); - const scrollState = useScrollState( - tabsScrollContainerRef, - regularTabs.length, - ); + const scrollState = useScrollState(tabsScrollContainerRef, tabs.length); + + const LISTEN_BUTTON_EXPAND_SLACK_PX = 200; + const [hasOverflow, setHasOverflow] = useState(false); + useEffect(() => { + setHasOverflow((prev) => { + if (!prev && scrollState.contentOverflowPx > 0) return true; + // Only exit compact when there's clearly enough room for the full button. + // contentOverflowPx can be negative (free space) because we measure the + // inner content element directly, not the scroll container. + if ( + prev && + scrollState.contentOverflowPx < -LISTEN_BUTTON_EXPAND_SLACK_PX + ) { + return false; + } + return prev; + }); + }, [scrollState.contentOverflowPx]); - const setTabRef = useScrollActiveTabIntoView(regularTabs); + const setTabRef = useScrollActiveTabIntoView(tabs); useTabsShortcuts(); return ( @@ -205,24 +213,7 @@ function Header({ tabs }: { tabs: Tab[] }) { )} - {listeningTab && ( -
- -
- )} - -
+
- - {regularTabs.map((tab, index) => { - const isLastTab = index === regularTabs.length - 1; - const shortcutIndex = listeningTab - ? index < 7 - ? index + 2 - : isLastTab - ? 9 - : undefined - : index < 8 - ? index + 1 - : isLastTab - ? 9 - : undefined; +
+ + {tabs.map((tab, index) => { + const isLastTab = index === tabs.length - 1; + const shortcutIndex = + index < 8 ? index + 1 : isLastTab ? 9 : undefined; - return ( - setTabRef(tab, el)} - style={{ position: "relative" }} - className="h-full z-10" - transition={{ layout: { duration: 0.15 } }} - > - - - ); - })} - + return ( + setTabRef(tab, el)} + style={{ position: "relative" }} + className="h-full z-10" + transition={{ layout: { duration: 0.15 } }} + > + + + ); + })} + + +
{!scrollState.atStart && ( -
+
)} {!scrollState.atEnd && ( -
+
)}
- - -
- - {!isOnboarding && ( - - )} -
+ + {!isOnboarding && ( + + )}
); @@ -696,7 +673,7 @@ export function StandardTabWrapper({ }) { return (
-
+
{children} {floatingButton} @@ -729,6 +706,7 @@ function useScrollState( const [scrollState, setScrollState] = useState({ atStart: true, atEnd: true, + contentOverflowPx: 0, }); const updateScrollState = useCallback(() => { @@ -736,13 +714,25 @@ function useScrollState( if (!container) return; const { scrollLeft, scrollWidth, clientWidth } = container; - const hasOverflow = scrollWidth > clientWidth + 1; + const containerOverflowPx = scrollWidth - clientWidth; + const hasScrollOverflow = containerOverflowPx > 1; + // Measure the inner content element (Reorder.Group) directly so that + // contentOverflowPx can be negative — representing free space — which + // enables proper hysteresis for the compact/full listen button transition. + const contentWidth = + container.firstElementChild?.scrollWidth ?? scrollWidth; + const contentOverflowPx = contentWidth - clientWidth; const newState = { - atStart: !hasOverflow || scrollLeft <= 1, - atEnd: !hasOverflow || scrollLeft + clientWidth >= scrollWidth - 1, + atStart: !hasScrollOverflow || scrollLeft <= 1, + atEnd: !hasScrollOverflow || scrollLeft + clientWidth >= scrollWidth - 1, + contentOverflowPx, }; setScrollState((prev) => { - if (prev.atStart === newState.atStart && prev.atEnd === newState.atEnd) { + if ( + prev.atStart === newState.atStart && + prev.atEnd === newState.atEnd && + prev.contentOverflowPx === newState.contentOverflowPx + ) { return prev; } return newState; diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx index 7460047fc6..ce507209a9 100644 --- a/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx +++ b/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx @@ -1,4 +1,3 @@ -import { useHover } from "@uidotdev/usehooks"; import { MicOff } from "lucide-react"; import { useCallback } from "react"; @@ -23,9 +22,18 @@ import { export function ListenButton({ sessionId }: { sessionId: string }) { const { shouldRender } = useListenButtonState(sessionId); const hasTranscript = useHasTranscript(sessionId); + const isActiveSession = useListener( + (state) => + (state.live.status === "active" || state.live.status === "finalizing") && + state.live.sessionId === sessionId, + ); + + if (isActiveSession) { + return ; + } if (!shouldRender) { - return ; + return null; } if (hasTranscript) { @@ -35,6 +43,25 @@ export function ListenButton({ sessionId }: { sessionId: string }) { return null; } +function DancingSticksIndicator() { + const { amplitude, muted } = useListener((state) => ({ + amplitude: state.live.amplitude, + muted: state.live.muted, + })); + + return ( +
+ {muted && } + +
+ ); +} + function StartButton({ sessionId }: { sessionId: string }) { const { isDisabled, warningMessage } = useListenButtonState(sessionId); const handleClick = useStartListening(sessionId); @@ -94,84 +121,3 @@ function StartButton({ sessionId }: { sessionId: string }) { ); } - -function InMeetingIndicator({ sessionId }: { sessionId: string }) { - const [ref, hovered] = useHover(); - - const { mode, stop, amplitude, muted } = useListener((state) => ({ - mode: state.getSessionMode(sessionId), - stop: state.stop, - amplitude: state.live.amplitude, - muted: state.live.muted, - })); - - const active = mode === "active" || mode === "finalizing"; - const finalizing = mode === "finalizing"; - - if (!active) { - return null; - } - - return ( - - - - - - {finalizing ? "Finalizing..." : "Stop listening"} - - - ); -} diff --git a/apps/desktop/src/components/main/body/sessions/shared.tsx b/apps/desktop/src/components/main/body/sessions/shared.tsx index 3941fbd5bd..5fa3554320 100644 --- a/apps/desktop/src/components/main/body/sessions/shared.tsx +++ b/apps/desktop/src/components/main/body/sessions/shared.tsx @@ -78,7 +78,7 @@ export function useCurrentNoteTab( } export function RecordingIcon() { - return
; + return
; } export function useListenButtonState(sessionId: string) { diff --git a/apps/desktop/src/components/main/body/shared.tsx b/apps/desktop/src/components/main/body/shared.tsx index 90ca20eafb..300f30f2c0 100644 --- a/apps/desktop/src/components/main/body/shared.tsx +++ b/apps/desktop/src/components/main/body/shared.tsx @@ -37,8 +37,18 @@ const accentColors: Record< } > = { neutral: { - selected: ["bg-neutral-50", "text-black", "border-stone-400"], - unselected: ["bg-neutral-50", "text-neutral-500", "border-transparent"], + selected: [ + "bg-neutral-50", + "hover:bg-stone-100", + "text-black", + "border-stone-400", + ], + unselected: [ + "bg-neutral-50", + "hover:bg-stone-100", + "text-neutral-500", + "border-transparent", + ], hover: { selected: "text-neutral-700 hover:text-neutral-900", unselected: "text-neutral-500 hover:text-neutral-700", diff --git a/apps/desktop/src/components/main/sidebar/index.tsx b/apps/desktop/src/components/main/sidebar/index.tsx index 92a5809166..1c1831772c 100644 --- a/apps/desktop/src/components/main/sidebar/index.tsx +++ b/apps/desktop/src/components/main/sidebar/index.tsx @@ -73,7 +73,6 @@ export function LeftSidebar() { "w-full h-9 py-1", isLinux ? "pl-3 justify-between" : "pl-20 justify-end", "shrink-0", - "rounded-xl bg-neutral-50", ])} > {isLinux && } @@ -149,7 +148,7 @@ export function LeftSidebar() { className={cn([ "text-sm placeholder:text-sm placeholder:text-neutral-400", "w-full pl-8 pr-8 py-1.5", - "rounded-lg bg-neutral-100", + "rounded-lg bg-neutral-200/50 border border-neutral-200", "focus:outline-hidden focus:bg-neutral-200", "transition-colors", ])} diff --git a/apps/desktop/src/components/main/sidebar/profile/index.tsx b/apps/desktop/src/components/main/sidebar/profile/index.tsx index 6af5227f19..79e7d7fbf9 100644 --- a/apps/desktop/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/index.tsx @@ -211,7 +211,7 @@ export function ProfileSection({ onExpandChange }: ProfileSectionProps = {}) { transition={{ duration: 0.2, ease: "easeInOut" }} className="absolute bottom-full left-0 right-0 mb-1" > -
+
{currentView === "main" ? ( @@ -268,7 +268,7 @@ export function ProfileSection({ onExpandChange }: ProfileSectionProps = {}) { )} -
+
setIsExpanded(!isExpanded)} @@ -311,12 +311,12 @@ function ProfileButton({ return (