Skip to content

Commit f989422

Browse files
Open notes in standalone windows
Adds a note-specific native window route and wires sidebar double-click to open the existing note view in that window. Includes regression coverage for the sidebar double-click path.
1 parent 2b9d773 commit f989422

10 files changed

Lines changed: 313 additions & 20 deletions

File tree

apps/desktop/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Route as AppIndexRouteImport } from './routes/app/index'
1414
import { Route as AppOnboardingRouteImport } from './routes/app/onboarding'
1515
import { Route as AppInstructionRouteImport } from './routes/app/instruction'
1616
import { Route as AppComposerRouteImport } from './routes/app/composer'
17+
import { Route as AppNoteSessionIdRouteImport } from './routes/app/note.$sessionId'
1718
import { Route as AppMainLayoutRouteImport } from './routes/app/main/_layout'
1819
import { Route as AppMainLayoutIndexRouteImport } from './routes/app/main/_layout.index'
1920

@@ -42,6 +43,11 @@ const AppComposerRoute = AppComposerRouteImport.update({
4243
path: '/composer',
4344
getParentRoute: () => AppRouteRoute,
4445
} as any)
46+
const AppNoteSessionIdRoute = AppNoteSessionIdRouteImport.update({
47+
id: '/note/$sessionId',
48+
path: '/note/$sessionId',
49+
getParentRoute: () => AppRouteRoute,
50+
} as any)
4551
const AppMainLayoutRoute = AppMainLayoutRouteImport.update({
4652
id: '/main/_layout',
4753
path: '/main',
@@ -60,13 +66,15 @@ export interface FileRoutesByFullPath {
6066
'/app/onboarding': typeof AppOnboardingRoute
6167
'/app/': typeof AppIndexRoute
6268
'/app/main': typeof AppMainLayoutRouteWithChildren
69+
'/app/note/$sessionId': typeof AppNoteSessionIdRoute
6370
'/app/main/': typeof AppMainLayoutIndexRoute
6471
}
6572
export interface FileRoutesByTo {
6673
'/app/composer': typeof AppComposerRoute
6774
'/app/instruction': typeof AppInstructionRoute
6875
'/app/onboarding': typeof AppOnboardingRoute
6976
'/app': typeof AppIndexRoute
77+
'/app/note/$sessionId': typeof AppNoteSessionIdRoute
7078
'/app/main': typeof AppMainLayoutIndexRoute
7179
}
7280
export interface FileRoutesById {
@@ -77,6 +85,7 @@ export interface FileRoutesById {
7785
'/app/onboarding': typeof AppOnboardingRoute
7886
'/app/': typeof AppIndexRoute
7987
'/app/main/_layout': typeof AppMainLayoutRouteWithChildren
88+
'/app/note/$sessionId': typeof AppNoteSessionIdRoute
8089
'/app/main/_layout/': typeof AppMainLayoutIndexRoute
8190
}
8291
export interface FileRouteTypes {
@@ -88,13 +97,15 @@ export interface FileRouteTypes {
8897
| '/app/onboarding'
8998
| '/app/'
9099
| '/app/main'
100+
| '/app/note/$sessionId'
91101
| '/app/main/'
92102
fileRoutesByTo: FileRoutesByTo
93103
to:
94104
| '/app/composer'
95105
| '/app/instruction'
96106
| '/app/onboarding'
97107
| '/app'
108+
| '/app/note/$sessionId'
98109
| '/app/main'
99110
id:
100111
| '__root__'
@@ -104,6 +115,7 @@ export interface FileRouteTypes {
104115
| '/app/onboarding'
105116
| '/app/'
106117
| '/app/main/_layout'
118+
| '/app/note/$sessionId'
107119
| '/app/main/_layout/'
108120
fileRoutesById: FileRoutesById
109121
}
@@ -148,6 +160,13 @@ declare module '@tanstack/react-router' {
148160
preLoaderRoute: typeof AppComposerRouteImport
149161
parentRoute: typeof AppRouteRoute
150162
}
163+
'/app/note/$sessionId': {
164+
id: '/app/note/$sessionId'
165+
path: '/note/$sessionId'
166+
fullPath: '/app/note/$sessionId'
167+
preLoaderRoute: typeof AppNoteSessionIdRouteImport
168+
parentRoute: typeof AppRouteRoute
169+
}
151170
'/app/main/_layout': {
152171
id: '/app/main/_layout'
153172
path: '/main'
@@ -183,6 +202,7 @@ interface AppRouteRouteChildren {
183202
AppOnboardingRoute: typeof AppOnboardingRoute
184203
AppIndexRoute: typeof AppIndexRoute
185204
AppMainLayoutRoute: typeof AppMainLayoutRouteWithChildren
205+
AppNoteSessionIdRoute: typeof AppNoteSessionIdRoute
186206
}
187207

188208
const AppRouteRouteChildren: AppRouteRouteChildren = {
@@ -191,6 +211,7 @@ const AppRouteRouteChildren: AppRouteRouteChildren = {
191211
AppOnboardingRoute: AppOnboardingRoute,
192212
AppIndexRoute: AppIndexRoute,
193213
AppMainLayoutRoute: AppMainLayoutRouteWithChildren,
214+
AppNoteSessionIdRoute: AppNoteSessionIdRoute,
194215
}
195216

196217
const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { createFileRoute } from "@tanstack/react-router";
2+
import { useMemo } from "react";
3+
4+
import { ClassicMainLayout } from "~/main/layout";
5+
import { TabContentNote } from "~/session";
6+
import { StandaloneWindowShell } from "~/shared/window-shell";
7+
import type { Tab } from "~/store/zustand/tabs";
8+
9+
export const Route = createFileRoute("/app/note/$sessionId")({
10+
component: Component,
11+
});
12+
13+
function Component() {
14+
const { sessionId } = Route.useParams();
15+
const tab = useMemo(
16+
() =>
17+
({
18+
active: true,
19+
id: sessionId,
20+
pinned: false,
21+
slotId: `note-window-${sessionId}`,
22+
state: { view: null, autoStart: null },
23+
type: "sessions",
24+
}) satisfies Extract<Tab, { type: "sessions" }>,
25+
[sessionId],
26+
);
27+
28+
return (
29+
<ClassicMainLayout includeServices={false}>
30+
<StandaloneWindowShell>
31+
<div className="bg-background h-screen w-screen">
32+
<TabContentNote tab={tab} standaloneWindow />
33+
</div>
34+
</StandaloneWindowShell>
35+
</ClassicMainLayout>
36+
);
37+
}

apps/desktop/src/session/components/outer-header/index.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import { useListener } from "~/stt/contexts";
2020
export function OuterHeader({
2121
sessionId,
2222
currentView,
23+
standaloneWindow = false,
2324
title,
2425
}: {
2526
sessionId: string;
2627
currentView: EditorView;
28+
standaloneWindow?: boolean;
2729
title?: React.ReactNode;
2830
}) {
2931
const { leftsidebar } = useShell();
@@ -48,11 +50,13 @@ export function OuterHeader({
4850
className={cn([
4951
"pointer-events-none absolute inset-y-0 flex items-center",
5052
reserveCollapsedLiveControls ? "right-[153px]" : "right-[70px]",
51-
showSidebarTimelineHeaderGutter
52-
? "left-[104px]"
53-
: showExpandedSidebarTimelineHeader
54-
? "left-0"
55-
: "left-[114px]",
53+
standaloneWindow
54+
? "left-[68px]"
55+
: showSidebarTimelineHeaderGutter
56+
? "left-[104px]"
57+
: showExpandedSidebarTimelineHeader
58+
? "left-0"
59+
: "left-[114px]",
5660
])}
5761
>
5862
<div

apps/desktop/src/session/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ import { useSTTConnection } from "~/stt/useSTTConnection";
2626
import { useUploadFile } from "~/stt/useUploadFile";
2727

2828
export function TabContentNote({
29+
standaloneWindow = false,
2930
tab,
3031
}: {
32+
standaloneWindow?: boolean;
3133
tab: Extract<Tab, { type: "sessions" }>;
3234
}) {
3335
const sessionMode = useListener((state) => state.getSessionMode(tab.id));
@@ -89,6 +91,7 @@ export function TabContentNote({
8991
<AudioPlayer.Provider sessionId={tab.id} url={audioUrl ?? ""}>
9092
<TabContentNoteInner
9193
tab={tab}
94+
standaloneWindow={standaloneWindow}
9295
audioUrlReady={Boolean(audioUrl)}
9396
isAudioUrlLoading={audioUrlQuery.isPending}
9497
/>
@@ -100,10 +103,12 @@ export function TabContentNote({
100103

101104
function TabContentNoteInner({
102105
tab,
106+
standaloneWindow,
103107
audioUrlReady,
104108
isAudioUrlLoading,
105109
}: {
106110
tab: Extract<Tab, { type: "sessions" }>;
111+
standaloneWindow: boolean;
107112
audioUrlReady: boolean;
108113
isAudioUrlLoading: boolean;
109114
}) {
@@ -228,6 +233,7 @@ function TabContentNoteInner({
228233
<OuterHeader
229234
sessionId={tab.id}
230235
currentView={currentView}
236+
standaloneWindow={standaloneWindow}
231237
title={
232238
<TitleInput
233239
ref={titleInputRef}

apps/desktop/src/shared/ui/interactive-button.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {
33
type MouseEvent,
44
type ReactNode,
55
useCallback,
6+
useRef,
67
} from "react";
78

9+
import { useMountEffect } from "~/shared/hooks/useMountEffect";
810
import {
911
type MenuItemDef,
1012
useNativeContextMenu,
@@ -13,6 +15,7 @@ import {
1315
interface InteractiveButtonProps {
1416
children: ReactNode;
1517
onClick?: () => void;
18+
onDoubleClick?: () => void;
1619
onCmdClick?: () => void;
1720
onShiftClick?: () => void;
1821
onMouseDown?: (e: MouseEvent<HTMLElement>) => void;
@@ -27,6 +30,7 @@ interface InteractiveButtonProps {
2730
export function InteractiveButton({
2831
children,
2932
onClick,
33+
onDoubleClick,
3034
onCmdClick,
3135
onShiftClick,
3236
onMouseDown,
@@ -38,6 +42,16 @@ export function InteractiveButton({
3842
asChild = false,
3943
}: InteractiveButtonProps) {
4044
const showMenu = useNativeContextMenu(contextMenu ?? []);
45+
const clickTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
46+
47+
const clearPendingClick = useCallback(() => {
48+
if (clickTimeoutRef.current) {
49+
clearTimeout(clickTimeoutRef.current);
50+
clickTimeoutRef.current = null;
51+
}
52+
}, []);
53+
54+
useMountEffect(() => clearPendingClick);
4155

4256
const handleClick = useCallback(
4357
(e: MouseEvent<HTMLElement>) => {
@@ -46,23 +60,52 @@ export function InteractiveButton({
4660
}
4761

4862
if (e.shiftKey) {
63+
clearPendingClick();
4964
e.preventDefault();
5065
onShiftClick?.();
5166
} else if (e.metaKey || e.ctrlKey) {
67+
clearPendingClick();
5268
e.preventDefault();
5369
onCmdClick?.();
70+
} else if (onDoubleClick) {
71+
clearPendingClick();
72+
clickTimeoutRef.current = setTimeout(() => {
73+
clickTimeoutRef.current = null;
74+
onClick?.();
75+
}, 200);
5476
} else {
5577
onClick?.();
5678
}
5779
},
58-
[onClick, onCmdClick, onShiftClick, disabled],
80+
[
81+
onClick,
82+
onDoubleClick,
83+
onCmdClick,
84+
onShiftClick,
85+
disabled,
86+
clearPendingClick,
87+
],
88+
);
89+
90+
const handleDoubleClick = useCallback(
91+
(e: MouseEvent<HTMLElement>) => {
92+
if (disabled) {
93+
return;
94+
}
95+
96+
clearPendingClick();
97+
e.preventDefault();
98+
onDoubleClick?.();
99+
},
100+
[onDoubleClick, disabled, clearPendingClick],
59101
);
60102

61103
const Element = asChild ? "div" : "button";
62104

63105
return (
64106
<Element
65107
onClick={handleClick}
108+
onDoubleClick={onDoubleClick ? handleDoubleClick : undefined}
66109
onDragStart={onDragStart}
67110
onMouseDown={onMouseDown}
68111
onContextMenu={contextMenu ? showMenu : undefined}

0 commit comments

Comments
 (0)