Skip to content

Commit 7bd6d0c

Browse files
committed
feat: first-launch welcome dialog
Wire up the welcome dialog shown once on first run (documented in the v0.6.6 changelog but never committed): App renders <WelcomeDialog /> alongside the post-update changelog. The dialog uses animate-fade-in so it does not flash off-center on open, matching the other centered dialogs.
1 parent 0190fde commit 7bd6d0c

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

apps/desktop/src/renderer/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SplitPane, type SplitOrientation } from './components/SplitPane.js';
1212
import { GitStatusBar } from './components/GitStatusBar.js';
1313
import { ParallelBurstHud } from './components/ParallelBurstHud.js';
1414
import { PostUpdateChangelog } from './components/PostUpdateChangelog.js';
15+
import { WelcomeDialog } from './components/WelcomeDialog.js';
1516
import { useAppStore } from './store.js';
1617
import { bridge } from './bridge.js';
1718
import { usePlatform } from './hooks/usePlatform.js';
@@ -437,6 +438,7 @@ export function App(): JSX.Element {
437438
/>
438439
<ParallelBurstHud />
439440
<PostUpdateChangelog />
441+
<WelcomeDialog />
440442
<RunnerPanel />
441443
</div>
442444
);
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as RadixDialog from '@radix-ui/react-dialog';
2+
import { useEffect, useState } from 'react';
3+
import { bridge } from '../bridge.js';
4+
import { useAppStore } from '../store.js';
5+
import { shortcutLabel } from '../hooks/useShortcuts.js';
6+
7+
const WELCOME_SEEN_KEY = 'app:welcomeSeen';
8+
9+
/**
10+
* First-launch welcome dialog. Shown exactly once per machine — on
11+
* any subsequent launch the `app:welcomeSeen` localStorage flag
12+
* suppresses it. Skipped entirely when a workspace is already open
13+
* (the user got there via an installer that pre-seeded one, or
14+
* they previously hit the post-update changelog dialog which counts
15+
* as having seen the app).
16+
*
17+
* The post-update changelog dialog has its own flag so a fresh
18+
* install on a workspace-less machine sees only this welcome, never
19+
* a changelog they didn't author.
20+
*/
21+
export function WelcomeDialog(): JSX.Element | null {
22+
const workspace = useAppStore((s) => s.workspace);
23+
const recents = useAppStore((s) => s.recents);
24+
const openWorkspace = useAppStore((s) => s.openWorkspace);
25+
const newTab = useAppStore((s) => s.newTab);
26+
const focusUrl = useAppStore((s) => s.focusUrl);
27+
28+
const [open, setOpen] = useState(false);
29+
30+
useEffect(() => {
31+
if (typeof window === 'undefined') return;
32+
let seen = false;
33+
try {
34+
seen = window.localStorage.getItem(WELCOME_SEEN_KEY) === 'true';
35+
} catch {
36+
return;
37+
}
38+
if (seen) return;
39+
setOpen(true);
40+
}, []);
41+
42+
const dismiss = (): void => {
43+
setOpen(false);
44+
try {
45+
window.localStorage.setItem(WELCOME_SEEN_KEY, 'true');
46+
} catch {
47+
/* ignore */
48+
}
49+
};
50+
51+
const handleNewTab = (): void => {
52+
newTab();
53+
focusUrl();
54+
dismiss();
55+
};
56+
57+
const handleOpenWorkspace = async (): Promise<void> => {
58+
const picked = await bridge.workspacePickDir();
59+
if (picked) {
60+
await openWorkspace(picked);
61+
dismiss();
62+
}
63+
};
64+
65+
return (
66+
<RadixDialog.Root open={open} onOpenChange={(next) => !next && dismiss()}>
67+
<RadixDialog.Portal>
68+
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-ink-1/20 backdrop-blur-[2px] animate-fade-in" />
69+
<RadixDialog.Content className="fixed left-1/2 top-1/2 z-50 flex max-h-[85vh] w-[560px] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-lg border border-line bg-bg-canvas shadow-popover animate-fade-in">
70+
<div className="px-6 pt-6">
71+
<RadixDialog.Title className="text-base font-semibold text-ink-1">
72+
Welcome to Scrapeman
73+
</RadixDialog.Title>
74+
<RadixDialog.Description className="mt-1 text-xs text-ink-3">
75+
A local-first API client built for scraping engineers. Three quick paths in.
76+
</RadixDialog.Description>
77+
</div>
78+
79+
<div className="flex-1 overflow-y-auto px-6 py-5">
80+
<div className="grid grid-cols-1 gap-2">
81+
<QuickStartCard
82+
title="Start a request"
83+
description="Open a draft tab with an empty URL bar. Type or paste a curl command to import."
84+
action="Open URL bar"
85+
onClick={handleNewTab}
86+
shortcut={shortcutLabel('mod+t')}
87+
/>
88+
<QuickStartCard
89+
title="Open a workspace"
90+
description="Point at a folder full of .sman files — your collection becomes the sidebar tree. Git-friendly out of the box."
91+
action="Pick folder…"
92+
onClick={() => void handleOpenWorkspace()}
93+
/>
94+
{recents.length > 0 && (
95+
<div className="rounded-md border border-line bg-bg-subtle px-3 py-2">
96+
<div className="text-[10px] font-semibold uppercase tracking-wide text-ink-3">
97+
Recent workspaces
98+
</div>
99+
<ul className="mt-1.5 space-y-0.5">
100+
{recents.slice(0, 5).map((r) => (
101+
<li key={r.path}>
102+
<button
103+
type="button"
104+
onClick={() => {
105+
void openWorkspace(r.path);
106+
dismiss();
107+
}}
108+
className="block w-full truncate rounded px-2 py-1 text-left text-xs text-ink-2 hover:bg-bg-hover hover:text-ink-1"
109+
title={r.path}
110+
>
111+
<span className="font-medium">{r.name}</span>
112+
<span className="ml-2 text-ink-4">{r.path}</span>
113+
</button>
114+
</li>
115+
))}
116+
</ul>
117+
</div>
118+
)}
119+
</div>
120+
121+
<div className="mt-5">
122+
<div className="text-[10px] font-semibold uppercase tracking-wide text-ink-3">
123+
Top shortcuts
124+
</div>
125+
<div className="mt-2 grid grid-cols-2 gap-1 text-xs">
126+
<ShortcutHint combo="mod+enter" label="Send request" />
127+
<ShortcutHint combo="mod+k" label="Command palette" />
128+
<ShortcutHint combo="mod+t" label="New tab" />
129+
<ShortcutHint combo="mod+l" label="Focus URL bar" />
130+
<ShortcutHint combo="mod+s" label="Save" />
131+
<ShortcutHint combo="mod+b" label="Toggle sidebar" />
132+
</div>
133+
<div className="mt-2 text-[11px] text-ink-4">
134+
See the full list in Settings → Keyboard shortcuts.
135+
</div>
136+
</div>
137+
</div>
138+
139+
<div className="flex items-center justify-between border-t border-line bg-bg-subtle px-6 py-3">
140+
<a
141+
href="https://scrapeman.app/docs"
142+
target="_blank"
143+
rel="noreferrer"
144+
className="text-[11px] text-ink-3 hover:text-ink-1"
145+
>
146+
Read the docs ↗
147+
</a>
148+
<button type="button" onClick={dismiss} className="btn-secondary">
149+
Get started
150+
</button>
151+
</div>
152+
</RadixDialog.Content>
153+
</RadixDialog.Portal>
154+
</RadixDialog.Root>
155+
);
156+
}
157+
158+
function QuickStartCard({
159+
title,
160+
description,
161+
action,
162+
onClick,
163+
shortcut,
164+
}: {
165+
title: string;
166+
description: string;
167+
action: string;
168+
onClick: () => void;
169+
shortcut?: string;
170+
}): JSX.Element {
171+
return (
172+
<button
173+
type="button"
174+
onClick={onClick}
175+
className="group flex flex-col gap-1 rounded-md border border-line bg-bg-canvas px-3 py-2.5 text-left text-xs transition-colors hover:border-line-strong hover:bg-bg-hover"
176+
>
177+
<div className="flex items-baseline justify-between gap-2">
178+
<span className="font-semibold text-ink-1">{title}</span>
179+
<span className="font-mono text-[10px] text-accent group-hover:underline">
180+
{action}
181+
</span>
182+
</div>
183+
<span className="text-[11px] leading-relaxed text-ink-3">{description}</span>
184+
{shortcut && (
185+
<span className="mt-0.5 font-mono text-[10px] text-ink-4">{shortcut}</span>
186+
)}
187+
</button>
188+
);
189+
}
190+
191+
function ShortcutHint({
192+
combo,
193+
label,
194+
}: {
195+
combo: string;
196+
label: string;
197+
}): JSX.Element {
198+
return (
199+
<div className="flex items-center gap-2">
200+
<kbd className="shrink-0 rounded border border-line bg-bg-subtle px-1.5 py-0.5 font-mono text-[10px] text-ink-2">
201+
{shortcutLabel(combo)}
202+
</kbd>
203+
<span className="text-ink-3">{label}</span>
204+
</div>
205+
);
206+
}

0 commit comments

Comments
 (0)