Skip to content

Commit 0012f5b

Browse files
Merge pull request #1749 from generalaction/feat/v1-beta-entry-point
Core: In app hint to discover v1 beta
2 parents 56807f3 + 56ea043 commit 0012f5b

10 files changed

Lines changed: 122 additions & 17 deletions

File tree

src/renderer/components/ChangelogModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { MarkdownRenderer } from '@/components/ui/markdown-renderer';
44
import { DialogContent } from '@/components/ui/dialog';
55
import { formatChangelogPublishedAt } from '@/lib/changelogDate';
66
import { EMDASH_CHANGELOG_URL, type ChangelogEntry } from '@shared/changelog';
7-
import { EMDASH_WEBSITE_URL } from '@shared/urls';
7+
import { getEmdashV1BetaUrl } from '@shared/urls';
88
import { ArrowRight, ExternalLink } from 'lucide-react';
99

1010
interface ChangelogModalProps {
@@ -76,6 +76,7 @@ function ChangelogModal({ entry }: ChangelogModalProps): JSX.Element {
7676
const publishedAt = formatChangelogPublishedAt(entry.publishedAt);
7777
const content = stripLeadingReleaseHeadings(entry.content, entry);
7878
const { main, footer } = splitContentFooter(content);
79+
const betaUrl = getEmdashV1BetaUrl('changelog-modal');
7980

8081
return (
8182
<DialogContent className="max-w-2xl gap-0 overflow-hidden p-0 focus:outline-none">
@@ -93,7 +94,7 @@ function ChangelogModal({ entry }: ChangelogModalProps): JSX.Element {
9394
<div className="px-6 py-5">
9495
<button
9596
type="button"
96-
onClick={() => window.electronAPI.openExternal(EMDASH_WEBSITE_URL)}
97+
onClick={() => window.electronAPI.openExternal(betaUrl)}
9798
className="flex w-full items-center justify-between gap-3 rounded-xl border border-border/70 bg-muted/35 px-4 py-3 text-left transition-colors hover:bg-accent/40"
9899
>
99100
<div className="min-w-0">

src/renderer/components/ProjectMainView.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -793,9 +793,11 @@ const ProjectMainView: React.FC<ProjectMainViewProps> = ({
793793
<div className="mx-auto w-full max-w-6xl">
794794
{/* Header */}
795795
<div className="px-10">
796-
<header className="flex items-center justify-between">
797-
<h1 className="text-2xl font-semibold tracking-tight">{project.name}</h1>
798-
<div className="flex items-center gap-2">
796+
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
797+
<div className="min-w-0">
798+
<h1 className="text-2xl font-semibold tracking-tight">{project.name}</h1>
799+
</div>
800+
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
799801
<BaseBranchControls
800802
baseBranch={baseBranch}
801803
branchOptions={branchOptions}

src/renderer/components/automations/AutomationsView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ const AutomationsView: React.FC = () => {
110110
<div>
111111
<h1 className="flex items-center gap-2 text-lg font-semibold">
112112
Automations
113-
<span className="rounded bg-zinc-500/15 px-1.5 py-0.5 text-[9px] font-medium uppercase leading-none tracking-wide text-zinc-600 dark:bg-zinc-400/15 dark:text-zinc-400">
114-
Beta
113+
<span className="rounded bg-zinc-500/15 px-1.5 py-0.5 text-[9px] font-medium leading-none tracking-wide text-zinc-600 dark:bg-zinc-400/15 dark:text-zinc-400">
114+
beta
115115
</span>
116116
</h1>
117117
<p className="mt-1 text-xs text-muted-foreground">

src/renderer/components/sidebar/ChangelogNotificationCard.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { formatChangelogPublishedAt } from '@/lib/changelogDate';
33
import { cn } from '@/lib/utils';
44
import { motion } from 'framer-motion';
55
import type { ChangelogEntry } from '@shared/changelog';
6-
import { EMDASH_WEBSITE_URL } from '@shared/urls';
6+
import { getEmdashV1BetaUrl } from '@shared/urls';
77
import { ArrowRight, ExternalLink, X } from 'lucide-react';
88
import { useEmdashAccount } from '@/contexts/EmdashAccountProvider';
99
import { Button } from '@/components/ui/button';
@@ -25,6 +25,7 @@ export function ChangelogNotificationCard({
2525
}: ChangelogNotificationCardProps) {
2626
const publishedAt = formatChangelogPublishedAt(entry.publishedAt);
2727
const { hasAccount } = useEmdashAccount();
28+
const betaUrl = getEmdashV1BetaUrl('changelog-notification-card');
2829

2930
return (
3031
<motion.div
@@ -79,12 +80,12 @@ export function ChangelogNotificationCard({
7980
type="button"
8081
onClick={(event) => {
8182
event.stopPropagation();
82-
window.electronAPI.openExternal(EMDASH_WEBSITE_URL);
83+
window.electronAPI.openExternal(betaUrl);
8384
}}
8485
className="flex w-full items-center justify-between gap-3 rounded-lg border border-border/70 bg-muted/35 px-3 py-2 text-left transition-colors hover:bg-accent/40"
8586
>
8687
<div className="min-w-0">
87-
<p className="text-xs font-semibold text-foreground">Test the v1 Beta</p>
88+
<p className="text-xs font-semibold text-foreground">Test the v1 beta</p>
8889
<p className="mt-0.5 flex items-center gap-1 text-[11px] text-muted-foreground">
8990
<span>emdash.sh</span>
9091
<ExternalLink className="h-3 w-3" />

src/renderer/components/sidebar/LeftSidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,8 @@ export const LeftSidebar: React.FC<LeftSidebarProps> = ({
420420
>
421421
<Timer className="h-5 w-5 text-muted-foreground sm:h-4 sm:w-4" />
422422
<span className="text-sm font-medium">Automations</span>
423-
<span className="rounded bg-zinc-500/15 px-1.5 py-0.5 text-[9px] font-medium uppercase leading-none tracking-wide text-zinc-600 dark:bg-zinc-400/15 dark:text-zinc-400">
424-
Beta
423+
<span className="rounded bg-zinc-500/15 px-1.5 py-0.5 text-[9px] font-medium leading-none tracking-wide text-zinc-600 dark:bg-zinc-400/15 dark:text-zinc-400">
424+
beta
425425
</span>
426426
</Button>
427427
</SidebarMenuButton>

src/renderer/components/titlebar/Titlebar.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import {
77
Settings as SettingsIcon,
88
KanbanSquare,
99
Code2,
10+
ArrowUpRight,
1011
} from 'lucide-react';
1112
import { ShortcutHint } from '../ui/shortcut-hint';
1213
import SidebarLeftToggleButton from './SidebarLeftToggleButton';
1314
import SidebarRightToggleButton from './SidebarRightToggleButton';
1415
import { Button } from '../ui/button';
16+
import { Badge } from '../ui/badge';
1517
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
1618
import OpenInMenu from './OpenInMenu';
1719
import FeedbackModal from '../FeedbackModal';
@@ -24,6 +26,7 @@ import { useProjectManagementContext } from '../../contexts/ProjectManagementPro
2426
import { useTaskManagementContext } from '../../contexts/TaskManagementContext';
2527
import { useGithubContext } from '../../contexts/GithubContextProvider';
2628
import { useAppSettings } from '@/contexts/AppSettingsProvider';
29+
import { getEmdashV1BetaUrl } from '@shared/urls';
2730

2831
const isMacOS = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
2932

@@ -131,6 +134,7 @@ const Titlebar: React.FC<TitlebarProps> = ({
131134
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
132135
const feedbackButtonRef = useRef<HTMLButtonElement | null>(null);
133136
const headerRef = useRef<HTMLElement | null>(null);
137+
const betaUrl = getEmdashV1BetaUrl('titlebar-badge');
134138

135139
const handleOpenFeedback = useCallback(async () => {
136140
void import('../../lib/telemetryClient').then(({ captureTelemetry }) => {
@@ -219,11 +223,29 @@ const Titlebar: React.FC<TitlebarProps> = ({
219223
</div>
220224
)}
221225
<div
222-
className="pointer-events-auto flex flex-shrink-0 items-center [-webkit-app-region:no-drag]"
226+
className="pointer-events-auto flex flex-shrink-0 items-center gap-2 [-webkit-app-region:no-drag]"
223227
style={{
224228
paddingLeft: isMacOS ? 'env(titlebar-area-x, 80px)' : '0.5rem',
225229
}}
226230
>
231+
<Badge
232+
asChild
233+
variant="outline"
234+
className="border-emerald-700/18 hover:border-emerald-600/28 h-7 rounded-md bg-gradient-to-r from-emerald-950 via-emerald-900 to-teal-900 px-2.5 text-[11px] font-semibold text-emerald-50 shadow-sm shadow-emerald-950/20 transition-colors hover:from-emerald-900 hover:via-emerald-800 hover:to-teal-800 hover:text-white dark:border-emerald-500/30 dark:from-emerald-950 dark:via-emerald-900 dark:to-teal-900 dark:text-emerald-50 dark:hover:border-emerald-400/40 dark:hover:from-emerald-900 dark:hover:via-emerald-800 dark:hover:to-teal-800"
235+
>
236+
<a
237+
href={betaUrl}
238+
target="_blank"
239+
rel="noopener noreferrer"
240+
onClick={(event) => {
241+
event.preventDefault();
242+
void window.electronAPI.openExternal(betaUrl);
243+
}}
244+
>
245+
Try v1 beta now
246+
<ArrowUpRight className="size-3.5" data-icon="inline-end" />
247+
</a>
248+
</Badge>
227249
{showResourceMonitor ? <PerformanceChip /> : null}
228250
</div>
229251
{/* Center: project/task context (grows to fill) */}
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1-
import React from 'react';
1+
import * as React from 'react';
2+
import { Slot } from '@radix-ui/react-slot';
23
import { cn } from '@/lib/utils';
34

4-
type Props = React.HTMLAttributes<HTMLSpanElement> & {
5+
type Props = React.HTMLAttributes<HTMLElement> & {
56
variant?: 'default' | 'secondary' | 'outline';
7+
asChild?: boolean;
68
};
79

8-
export const Badge: React.FC<Props> = ({ className, variant = 'secondary', ...props }) => {
10+
export const Badge: React.FC<Props> = ({
11+
className,
12+
variant = 'secondary',
13+
asChild = false,
14+
...props
15+
}) => {
16+
const Comp = asChild ? Slot : 'span';
917
const base = 'inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium';
1018
const styles =
1119
variant === 'outline'
1220
? 'border border-border/70 bg-background text-foreground'
1321
: variant === 'default'
1422
? 'bg-foreground text-background'
1523
: 'border border-border/70 bg-muted/40 text-foreground';
16-
return <span className={cn(base, styles, className)} {...props} />;
24+
return <Comp className={cn(base, styles, className)} {...props} />;
1725
};
1826

1927
export default Badge;

src/shared/urls.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,34 @@
11
export const EMDASH_RELEASES_URL = 'https://github.com/generalaction/emdash/releases';
22
export const EMDASH_WEBSITE_URL = 'https://www.emdash.sh/';
3+
export const EMDASH_DOWNLOAD_URL = 'https://www.emdash.sh/download';
4+
5+
type UTMOptions = {
6+
source?: string;
7+
medium?: string;
8+
campaign: string;
9+
content?: string;
10+
term?: string;
11+
};
12+
13+
export function withUtmParams(url: string, utm: UTMOptions): string {
14+
const trackedUrl = new URL(url);
15+
trackedUrl.searchParams.set('utm_campaign', utm.campaign);
16+
17+
if (utm.source) trackedUrl.searchParams.set('utm_source', utm.source);
18+
if (utm.medium) trackedUrl.searchParams.set('utm_medium', utm.medium);
19+
if (utm.content) trackedUrl.searchParams.set('utm_content', utm.content);
20+
if (utm.term) trackedUrl.searchParams.set('utm_term', utm.term);
21+
22+
return trackedUrl.toString();
23+
}
24+
25+
export function getEmdashV1BetaUrl(utmContent?: string): string {
26+
return withUtmParams(EMDASH_DOWNLOAD_URL, {
27+
source: 'emdash-app',
28+
medium: 'in-app',
29+
campaign: 'v0-banner-link',
30+
content: utmContent,
31+
});
32+
}
333

434
export const EMDASH_DOCS_URL = 'https://emdash.sh/docs';

src/test/renderer/Badge.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, expect, it } from 'vitest';
3+
import { Badge } from '@/components/ui/badge';
4+
5+
describe('Badge', () => {
6+
it('renders links via asChild', () => {
7+
render(
8+
<Badge asChild>
9+
<a href="https://www.emdash.sh/">Try v1 beta now</a>
10+
</Badge>
11+
);
12+
13+
const link = screen.getByRole('link', { name: 'Try v1 beta now' });
14+
expect(link).toBeInTheDocument();
15+
expect(link).toHaveAttribute('href', 'https://www.emdash.sh/');
16+
expect(link).toHaveClass('inline-flex');
17+
});
18+
});

src/test/shared/urls.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { EMDASH_DOWNLOAD_URL, getEmdashV1BetaUrl, withUtmParams } from '../../shared/urls';
3+
4+
describe('shared urls', () => {
5+
it('builds tracked beta download urls with app attribution params', () => {
6+
const url = new URL(getEmdashV1BetaUrl('project-header-badge'));
7+
8+
expect(url.origin + url.pathname).toBe(EMDASH_DOWNLOAD_URL);
9+
expect(url.searchParams.get('utm_source')).toBe('emdash-app');
10+
expect(url.searchParams.get('utm_medium')).toBe('in-app');
11+
expect(url.searchParams.get('utm_campaign')).toBe('v0-banner-link');
12+
expect(url.searchParams.get('utm_content')).toBe('project-header-badge');
13+
});
14+
15+
it('preserves existing query params when appending UTMs', () => {
16+
const url = new URL(
17+
withUtmParams('https://www.emdash.sh/download?foo=bar', { campaign: 'test-campaign' })
18+
);
19+
20+
expect(url.searchParams.get('foo')).toBe('bar');
21+
expect(url.searchParams.get('utm_campaign')).toBe('test-campaign');
22+
});
23+
});

0 commit comments

Comments
 (0)