diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d3f0f5605..2dbf6724e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2479,6 +2479,9 @@ importers: '@electric-sql/pglite-repl': specifier: 0.3.5 version: 0.3.5(@babel/runtime@7.29.7)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.8)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.2)(@electric-sql/pglite@0.4.5)(@lezer/common@1.5.2)(codemirror@6.0.1(@lezer/common@1.5.2)) + lucide-vue-next: + specifier: ^0.561.0 + version: 0.561.0(vue@3.5.12(typescript@5.8.3)) posthog-js: specifier: ^1.236.4 version: 1.236.4 @@ -16252,6 +16255,12 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lucide-vue-next@0.561.0: + resolution: {integrity: sha512-c5HUckO0qHklVSOf/0vaSR3pEb8fYImRDCRDLde56uqS9js0D/e3RAvq0/YFWjkmyOBKCb0/IdskdoHZQEkT5g==} + deprecated: Package deprecated. Please use @lucide/vue instead. + peerDependencies: + vue: '>=3.0.1' + lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} @@ -39505,6 +39514,10 @@ snapshots: dependencies: react: 19.1.0 + lucide-vue-next@0.561.0(vue@3.5.12(typescript@5.8.3)): + dependencies: + vue: 3.5.12(typescript@5.8.3) + lunr@2.3.9: {} luxon@3.7.2: {} diff --git a/website/.vitepress/theme/components/MegaNav.vue b/website/.vitepress/theme/components/MegaNav.vue index af4da6cfa6..bb2f677fa9 100644 --- a/website/.vitepress/theme/components/MegaNav.vue +++ b/website/.vitepress/theme/components/MegaNav.vue @@ -89,6 +89,14 @@ const NAV = [ }, }, '|', + // App sits in its own visual group between the infra products + // (Agents / Streams / Sync) and the cloud / pricing cluster, with a + // divider on each side. It's neither an infra primitive nor cloud + // infrastructure — it's the desktop + mobile client surface for the + // whole platform, so it gets its own slot in the bar. Plain link + // (no dropdown) because the page is self-contained. + { id: 'app', label: 'App', link: '/app' }, + '|', { id: 'cloud', label: 'Cloud', @@ -173,6 +181,7 @@ const activeId = computed(() => { if (p.startsWith('/agents') || p.startsWith('/docs/agents')) return 'agents' if (p.startsWith('/streams') || p.startsWith('/docs/streams')) return 'streams' if (p.startsWith('/sync') || p.startsWith('/docs/sync')) return 'sync' + if (p.startsWith('/app')) return 'app' if (p.startsWith('/cloud')) return 'cloud' if (p.startsWith('/pricing')) return 'pricing' if (p.startsWith('/blog')) return 'blog' diff --git a/website/.vitepress/theme/components/MegaNavMobile.vue b/website/.vitepress/theme/components/MegaNavMobile.vue index 03278559c9..bd9e268b8d 100644 --- a/website/.vitepress/theme/components/MegaNavMobile.vue +++ b/website/.vitepress/theme/components/MegaNavMobile.vue @@ -39,6 +39,8 @@ const NAV = [ ], }, '|', + { id: 'app', label: 'App', link: '/app' }, + '|', { id: 'cloud', label: 'Cloud', diff --git a/website/package.json b/website/package.json index 430ce5199b..1495c13098 100644 --- a/website/package.json +++ b/website/package.json @@ -47,6 +47,7 @@ "dependencies": { "@electric-sql/pglite": "0.4.5", "@electric-sql/pglite-repl": "0.3.5", + "lucide-vue-next": "^0.561.0", "posthog-js": "^1.236.4" } } diff --git a/website/src/components/app-download/AdPlaceholder.vue b/website/src/components/app-download/AdPlaceholder.vue new file mode 100644 index 0000000000..f187aac4c1 --- /dev/null +++ b/website/src/components/app-download/AdPlaceholder.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/website/src/components/app-download/AppDownloadPage.vue b/website/src/components/app-download/AppDownloadPage.vue index c0d283540e..eed9ff8c95 100644 --- a/website/src/components/app-download/AppDownloadPage.vue +++ b/website/src/components/app-download/AppDownloadPage.vue @@ -23,15 +23,20 @@ import { VPButton } from 'vitepress/theme' import Section from '../agents-home/Section.vue' import BottomCtaStrap from '../BottomCtaStrap.vue' +import AppMockupShadowHost from '../brand-toys/app/AppMockupShadowHost.vue' +import HeroChatStateScene from '../brand-toys/app/scenes/desktop/HeroChatStateScene.vue' +import HeroMobileChatScene from '../brand-toys/app/scenes/mobile/HeroMobileChatScene.vue' const githubReleaseBase = `https://github.com/electric-sql/electric/releases` const appReleaseNotesUrl = `${githubReleaseBase}?q=%22%40electric-ax%2Fagents-desktop%22&expanded=true` +const agentsMobileRepoUrl = `https://github.com/electric-sql/electric/tree/main/packages/agents-mobile` type DesktopPlatformId = | 'macos-arm64' | 'macos-x64' | 'windows-x64' | 'linux-x64' +type MobilePlatformId = 'ios' | 'android' type DownloadOption = { label: string @@ -148,31 +153,6 @@ const canaryEntries: CanaryEntry[] = [ }, ] -type MobilePlatform = { - id: 'ios' | 'android' - icon: 'apple' | 'android' - storeIcon: 'appstore' | 'googleplay' - name: string - storeLabel: string -} - -const mobilePlatforms: MobilePlatform[] = [ - { - id: `ios`, - icon: `apple`, - storeIcon: `appstore`, - name: `iOS`, - storeLabel: `App Store`, - }, - { - id: `android`, - icon: `android`, - storeIcon: `googleplay`, - name: `Android`, - storeLabel: `Google Play`, - }, -] - function releaseUrl(tag: string, assetName: string): string { return `${githubReleaseBase}/download/${encodeURIComponent(tag)}/${assetName}` } @@ -184,6 +164,7 @@ function latestReleaseUrl(assetName: string): string { /* Detect the visitor's OS on mount; default to macOS Apple Silicon so SSR / first paint always renders a sensible primary. */ const detectedId = ref('macos-arm64') +const detectedMobileId = ref(null) /* All Mac browsers still report `Intel Mac OS X` in the UA string on Apple Silicon for legacy compat (it's a deliberate Apple/browser @@ -214,7 +195,13 @@ function detectMacArch(): 'macos-arm64' | 'macos-x64' { onMounted(() => { if (typeof navigator === 'undefined') return const ua = `${navigator.userAgent || ''} ${navigator.platform || ''}` - if (/Win(dows|64|32)|WOW64|WinNT/i.test(ua)) { + const isIPadDesktopMode = + /Mac|Macintosh/i.test(ua) && (navigator.maxTouchPoints ?? 0) > 1 + if (/Android/i.test(ua)) { + detectedMobileId.value = 'android' + } else if (/iPhone|iPad|iPod/i.test(ua) || isIPadDesktopMode) { + detectedMobileId.value = 'ios' + } else if (/Win(dows|64|32)|WOW64|WinNT/i.test(ua)) { detectedId.value = 'windows-x64' } else if ( /Linux|X11|Ubuntu|Fedora|Debian/i.test(ua) && @@ -231,6 +218,25 @@ const primaryPlatform = computed( desktopPlatforms.find((p) => p.id === detectedId.value) ?? desktopPlatforms[0] ) + +const primaryCta = computed(() => { + if (detectedMobileId.value === 'ios') { + return { + label: 'iOS preview', + href: '#mobile', + } + } + if (detectedMobileId.value === 'android') { + return { + label: 'Android preview', + href: '#mobile', + } + } + return { + label: primaryPlatform.value.downloads[0].label, + href: latestReleaseUrl(primaryPlatform.value.downloads[0].assetName), + } +})