Merge marketing site and promo video update
@@ -0,0 +1,42 @@
|
|||||||
|
name: Deploy Marketing Site
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'site/**'
|
||||||
|
- '.github/workflows/deploy-site.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
- run: cd site && npm ci && npm run build
|
||||||
|
- uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: site/dist
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -9,3 +9,8 @@ local.properties
|
|||||||
*.hprof
|
*.hprof
|
||||||
.recovery-backups/
|
.recovery-backups/
|
||||||
.superpowers/
|
.superpowers/
|
||||||
|
.env
|
||||||
|
.wrangler/
|
||||||
|
.edgeone/
|
||||||
|
.tef_dist/
|
||||||
|
site/.edgeone/
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ android {
|
|||||||
applicationId "ai.multicasual.app"
|
applicationId "ai.multicasual.app"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 2
|
versionCode 3
|
||||||
versionName "0.1.1"
|
versionName "0.1.2"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="Multi-Casual Android is an independent Android companion app for Multica users." />
|
<meta name="description" content="Multi-Casual Android is an independent Android companion app for Multica users." />
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='18' fill='%231647ff'/%3E%3Ctext x='32' y='41' text-anchor='middle' font-family='Arial,sans-serif' font-size='30' font-weight='800' fill='white'%3EM%3C/text%3E%3C/svg%3E" />
|
<link rel="icon" href="/favicon.png" type="image/png" />
|
||||||
<title>Multi-Casual Android</title>
|
<title>Multi-Casual Android</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -264,6 +264,37 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 140 KiB |
@@ -1,8 +1,21 @@
|
|||||||
import { useState } from "react";
|
import logoImg from "./assets/logo.png";
|
||||||
import { motion, useReducedMotion, type MotionProps } from "motion/react";
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
import issueDetail from "./assets/screenshots/02-issue-detail.png";
|
import { motion, AnimatePresence, useMotionValue, useTransform, useReducedMotion } from "motion/react";
|
||||||
import commentFocused from "./assets/screenshots/03-comment-focused.png";
|
import inboxImg from "./assets/screenshots/01-inbox.jpg";
|
||||||
import promoVideo from "./assets/video/multica-android-promo-funk-final.mp4";
|
import issuesListImg from "./assets/screenshots/02-issues-list.jpg";
|
||||||
|
import issuesBoardImg from "./assets/screenshots/03-issues-board.jpg";
|
||||||
|
import issueDetailImg from "./assets/screenshots/04-issue-detail.jpg";
|
||||||
|
import issueSubImg from "./assets/screenshots/05-issue-detail-subissues.jpg";
|
||||||
|
import commentImg from "./assets/screenshots/06-comment-composer.jpg";
|
||||||
|
import mentionImg from "./assets/screenshots/07-mention-picker.jpg";
|
||||||
|
import projectsImg from "./assets/screenshots/08-projects-list.jpg";
|
||||||
|
import projectDetailImg from "./assets/screenshots/09-project-detail.jpg";
|
||||||
|
import agentsImg from "./assets/screenshots/10-agents-list.jpg";
|
||||||
|
import runtimesImg from "./assets/screenshots/11-runtimes-list.jpg";
|
||||||
|
import skillsImg from "./assets/screenshots/12-skills-list.jpg";
|
||||||
|
import skillDetailImg from "./assets/screenshots/13-skill-detail.jpg";
|
||||||
|
import settingsImg from "./assets/screenshots/14-settings.jpg";
|
||||||
|
import promoVideo from "./assets/video/multica-android-promo-draft.mp4";
|
||||||
|
|
||||||
const release = {
|
const release = {
|
||||||
packageName: "ai.multicasual.app",
|
packageName: "ai.multicasual.app",
|
||||||
@@ -10,309 +23,603 @@ const release = {
|
|||||||
githubUrl: "https://github.com/park0er/multi-casual-android",
|
githubUrl: "https://github.com/park0er/multi-casual-android",
|
||||||
releasesUrl: "https://github.com/park0er/multi-casual-android/releases/latest",
|
releasesUrl: "https://github.com/park0er/multi-casual-android/releases/latest",
|
||||||
multicaUrl: "https://multica.ai",
|
multicaUrl: "https://multica.ai",
|
||||||
serviceUrl: "https://app.multica.ai",
|
|
||||||
apiUrl: "https://api.multica.ai",
|
apiUrl: "https://api.multica.ai",
|
||||||
};
|
};
|
||||||
|
|
||||||
type Locale = "en" | "zh";
|
type Locale = "en" | "zh";
|
||||||
|
|
||||||
const copy = {
|
const copy: Record<Locale, {
|
||||||
|
nav: { brand: string; download: string; trust: string; language: string };
|
||||||
|
hero: { eyebrow: string; title: string; subtitle: string; download: string; github: string; multica: string };
|
||||||
|
relationship: { kicker: string; title: string; body1: string; body2: string };
|
||||||
|
journey: { kicker: string; title: string; subtitle: string; cta: string; steps: Array<{ title: string; subtitle: string }> };
|
||||||
|
workspace: { kicker: string; title: string; subtitle: string; items: Array<{ title: string; subtitle: string }> };
|
||||||
|
video: { kicker: string; title: string; body: string };
|
||||||
|
download: { kicker: string; title: string; package: string; api: string; sha: string; latest: string; source: string; star: string };
|
||||||
|
trust: { kicker: string; items: string[] };
|
||||||
|
footer: { tagline: string; multica: string; github: string; download: string };
|
||||||
|
}> = {
|
||||||
en: {
|
en: {
|
||||||
nav: {
|
nav: { brand: "Multi-Casual Android", download: "Download", trust: "Trust", language: "中文" },
|
||||||
brand: "Multi-Casual Android",
|
|
||||||
download: "Download",
|
|
||||||
trust: "Trust",
|
|
||||||
relationship: "Relationship",
|
|
||||||
language: "中文",
|
|
||||||
},
|
|
||||||
hero: {
|
hero: {
|
||||||
eyebrow: "Independent companion app · Built for Multica workflows",
|
eyebrow: "Independent companion app · Built for Multica workflows",
|
||||||
title: "Review issues, reply faster, stay in flow on Android.",
|
title: "Keep your Multica workspace moving from Android.",
|
||||||
subtitle:
|
subtitle: "Check inbox updates, browse issues, read full context, reply in threads, and keep agent workspace assets close — an independent Android companion app for Multica users.",
|
||||||
"An independent Android companion app for people who already use Multica and want the core workspace loop on a phone.",
|
|
||||||
download: "Download APK",
|
download: "Download APK",
|
||||||
github: "View GitHub",
|
github: "GitHub",
|
||||||
multica: "Visit Multica",
|
multica: "Visit Multica",
|
||||||
typing: "Looks good — verified on Android.",
|
|
||||||
},
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
kicker: "Relationship to Multica",
|
kicker: "Relationship to Multica",
|
||||||
title: "Respectful, explicit, and independent.",
|
title: "Respectful, explicit, and independent.",
|
||||||
body:
|
body1: "Multi-Casual Android is an independent companion app for Multica users. It connects to Multica service APIs for compatibility, but it is not maintained, endorsed, or published by the Multica project maintainers.",
|
||||||
"Multi-Casual Android is an independent companion app for Multica users. It connects to Multica service APIs for compatibility, but it is not maintained, endorsed, or published by the Multica project maintainers, and it should not be presented as a Multica Android app from the upstream project.",
|
body2: "This project was originally explored as a contribution path with the Multica community. The Multica maintainers later indicated that their own app direction would be led upstream, so this Android client is maintained independently, following that boundary while staying respectful of the original project.",
|
||||||
},
|
},
|
||||||
featuresKicker: "What it covers",
|
journey: {
|
||||||
features: [
|
kicker: "Interactive walkthrough",
|
||||||
{
|
title: "Try the flow.",
|
||||||
title: "Issue workflows",
|
subtitle: "Click each step or scroll to see the app in action.",
|
||||||
body: "Browse inbox items, open issue details, review comments, and keep task context close while away from desktop.",
|
cta: "Start the walkthrough",
|
||||||
},
|
steps: [
|
||||||
{
|
{ title: "Inbox notifications", subtitle: "Stay on top of activity — new issues, comments, and agent progress show up as they happen." },
|
||||||
title: "Mobile replies",
|
{ title: "Browse issues", subtitle: "See all tasks in a clean list view, ready for triage. Filter, search, and tap into any issue." },
|
||||||
body: "Tap into the comment flow, draft a response, and continue the same discussion you would normally handle on the web.",
|
{ title: "Switch to board view", subtitle: "Tap the view toggle to switch to a kanban board that shows status flow at a glance." },
|
||||||
},
|
{ title: "Open issue detail", subtitle: "Tap an issue card to see the full detail: title, status, labels, assignee, and body text." },
|
||||||
{
|
{ title: "Explore context", subtitle: "Context guide points zoom into focus areas — title and status, body content, then subtasks and comments — and return to the full view between each." },
|
||||||
title: "Cloud-service compatible",
|
{ title: "Scroll for more", subtitle: "Keep scrolling to see subtasks, subscribers, and the comment thread — all the structured context stays on one page." },
|
||||||
body: "The Android app is designed as a companion to the Multica backend service. It does not bundle a server, and it does not add runtime backend switching for normal users.",
|
{ title: "Draft a comment", subtitle: "Tap the comment box. The keyboard appears and a comment types out character by character." },
|
||||||
},
|
{ title: "Mention anyone", subtitle: "Type @ and a picker appears with people, agents, and squads. Choose who to notify." },
|
||||||
],
|
{ title: "Workspace overview", subtitle: "Switch between Projects, Agents, Runtimes, Skills, and Settings with workspace chips." },
|
||||||
demo: {
|
],
|
||||||
kicker: "Real Android flow",
|
|
||||||
title: "Open an issue. Read context. Draft a comment.",
|
|
||||||
body:
|
|
||||||
"The demo material uses emulator screenshots from a real project. Interactive moments are animated as safe overlays, so no public test comment needs to be submitted.",
|
|
||||||
},
|
},
|
||||||
video: {
|
workspace: {
|
||||||
kicker: "Promo video",
|
kicker: "Workspace assets",
|
||||||
title: "A quick walkthrough of the Android issue flow.",
|
title: "Projects, agents, and everything in between.",
|
||||||
body:
|
subtitle: "Switch between workspace views to see the full picture.",
|
||||||
"The walkthrough shows a real emulator project, issue context, a simulated typewriter-style comment draft, and a light funk-style background track.",
|
items: [
|
||||||
|
{ title: "Projects", subtitle: "Browse projects with progress tracking" },
|
||||||
|
{ title: "Project detail", subtitle: "Task stats, status fields, and resources" },
|
||||||
|
{ title: "Agents", subtitle: "View agent status and recent activity" },
|
||||||
|
{ title: "Runtimes", subtitle: "Check runtime availability and versions" },
|
||||||
|
{ title: "Skills", subtitle: "Browse the workspace agent skill library" },
|
||||||
|
{ title: "Skill detail", subtitle: "Inspect skill structure and documentation" },
|
||||||
|
{ title: "Settings", subtitle: "Account, workspace, and preference controls" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
download: {
|
video: { kicker: "Product demo", title: "See it in 30 seconds.", body: "A fast walkthrough of inbox, issues, context, comments, mentions, and workspace views." },
|
||||||
kicker: "GitHub APK",
|
download: { kicker: "GitHub APK", title: "Install from GitHub Releases.", package: "Package", api: "Service API", sha: "APK SHA256", latest: "Download APK", source: "Source code", star: "Like the app? Give us a star on GitHub ⭐ — it means a lot to the maintainers." },
|
||||||
title: "Install from GitHub Releases.",
|
trust: {
|
||||||
package: "Package",
|
kicker: "Trust and privacy",
|
||||||
api: "Service API",
|
items: [
|
||||||
sha: "APK SHA256",
|
"Community-maintained independent project; not affiliated with or endorsed by Multica maintainers.",
|
||||||
latest: "Latest release",
|
"Published as its own Android app for Multica users, with explicit attribution and product boundaries.",
|
||||||
source: "Source code",
|
"Open source under GPLv3 license — inspect, fork, or contribute.",
|
||||||
},
|
"No chat content, file content, email address, tokens, or precise location are collected.",
|
||||||
trustKicker: "Trust and privacy",
|
],
|
||||||
trustItems: [
|
|
||||||
"Independent companion app; no claim of Multica project ownership or maintainer endorsement.",
|
|
||||||
"Published as its own Android app for Multica users, while keeping attribution and product boundaries explicit.",
|
|
||||||
"Analytics requires user consent and is limited to an app_open event in the public Android build.",
|
|
||||||
"No chat content, file content, email address, tokens, or precise location are intentionally reported.",
|
|
||||||
],
|
|
||||||
footer: {
|
|
||||||
tagline: "Independent Android companion app for Multica users. GitHub-distributed APK.",
|
|
||||||
multica: "Multica website",
|
|
||||||
github: "GitHub",
|
|
||||||
download: "Download APK",
|
|
||||||
},
|
},
|
||||||
|
footer: { tagline: "Independent Android companion app for Multica users. GitHub-distributed APK.", multica: "Multica website", github: "GitHub", download: "Download APK" },
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
nav: {
|
nav: { brand: "Multi-Casual Android", download: "下载", trust: "信任说明", language: "English" },
|
||||||
brand: "Multi-Casual Android",
|
|
||||||
download: "下载",
|
|
||||||
trust: "信任说明",
|
|
||||||
relationship: "项目关系",
|
|
||||||
language: "English",
|
|
||||||
},
|
|
||||||
hero: {
|
hero: {
|
||||||
eyebrow: "独立 companion app · 面向 Multica 工作流",
|
eyebrow: "独立 companion app · 面向 Multica 工作流",
|
||||||
title: "在 Android 上处理 Issue。",
|
title: "在 Android 上跟进你的 Multica 工作现场。",
|
||||||
subtitle: "查看任务上下文、进入评论区、草拟回复,把核心 workspace 闭环带到手机上。",
|
subtitle: "查看通知、浏览 issues、阅读完整上下文、回复评论、@ 对方 —— 面向 Multica 用户的独立 Android companion app。",
|
||||||
download: "下载 APK",
|
download: "下载 APK",
|
||||||
github: "查看 GitHub",
|
github: "GitHub",
|
||||||
multica: "访问 Multica",
|
multica: "访问 Multica",
|
||||||
typing: "已在 Android 上验证,看起来没问题。",
|
|
||||||
},
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
kicker: "与 Multica 的关系",
|
kicker: "与 Multica 的关系",
|
||||||
title: "礼貌、清楚、保持独立。",
|
title: "礼貌、清楚、保持独立。",
|
||||||
body:
|
body1: "Multi-Casual Android 是面向 Multica 用户的独立 companion app。它为了兼容现有工作流而连接 Multica 服务 API,但并不由 Multica 项目维护者维护、背书或发布。",
|
||||||
"Multi-Casual Android 是面向 Multica 用户的独立 companion app。它为了兼容现有工作流而连接 Multica 服务 API,但并不由 Multica 项目维护者维护、背书或发布,也不应该被表述为上游项目推出的 Multica Android App。",
|
body2: "这个项目最初是作为向 Multica 社区贡献的路径来探索的。由于 Multica maintainers 后续计划推进他们自己主导的 app 体系,因此这套 Android 代码体系没有直接并入上游。我们尊重 maintainers 的边界建议,将 Multi-Casual Android 作为独立 app 继续运营和维护。",
|
||||||
},
|
},
|
||||||
featuresKicker: "覆盖能力",
|
journey: {
|
||||||
features: [
|
kicker: "交互式演示",
|
||||||
{
|
title: "试试这个流程。",
|
||||||
title: "Issue 工作流",
|
subtitle: "点击每个步骤,或滚动页面查看 app 实际操作。",
|
||||||
body: "查看 Inbox 条目、打开 Issue 详情、阅读评论,在离开桌面时也能保留任务上下文。",
|
cta: "开始演示",
|
||||||
},
|
steps: [
|
||||||
{
|
{ title: "Inbox 通知", subtitle: "新 issues、评论和 agent 进展会实时出现在收件箱,不错过任何动态。" },
|
||||||
title: "手机端回复",
|
{ title: "浏览 Issues", subtitle: "在列表视图中查看所有任务,快速筛选和进入任意 issue。" },
|
||||||
body: "进入评论输入流程,草拟回复,继续原本会在 Web 端处理的同一条讨论。",
|
{ title: "切换看板视图", subtitle: "点击视图切换按钮,看板视图让你一眼看到任务状态流。" },
|
||||||
},
|
{ title: "打开 Issue 详情", subtitle: "点击 issue 卡片查看完整详情:标题、状态、标签、负责人和正文。" },
|
||||||
{
|
{ title: "探索上下文", subtitle: "Context 引导点依次放大标题与状态、正文内容、子任务与评论,然后缩回全貌。" },
|
||||||
title: "兼容云服务流程",
|
{ title: "滚动查看更多", subtitle: "继续下滑查看子任务、订阅者和评论线程 —— 结构化上下文都在同一页。" },
|
||||||
body: "Android App 是 Multica 后端服务的 companion;不内置服务端,也不为普通用户提供运行时后端切换。",
|
{ title: "草拟评论", subtitle: "点击评论框,键盘出现,评论逐字输入。" },
|
||||||
},
|
{ title: "@ 提及任何人", subtitle: "输入 @ 弹出选择器,可选人、agent 或小队。" },
|
||||||
],
|
{ title: "Workspace 总览", subtitle: "通过 workspace chips 切换查看 Projects、Agents、Runtimes、Skills 和 Settings。" },
|
||||||
demo: {
|
],
|
||||||
kicker: "真实 Android 流程",
|
|
||||||
title: "打开 Issue,阅读上下文,然后草拟评论。",
|
|
||||||
body: "演示素材来自真实项目里的模拟器截图;交互部分用安全的叠加动画表现,不需要真的往公开项目里提交测试评论。",
|
|
||||||
},
|
},
|
||||||
video: {
|
workspace: {
|
||||||
kicker: "宣传视频",
|
kicker: "Workspace 资产",
|
||||||
title: "快速看一遍 Android 端 Issue 流程。",
|
title: "项目、agents,以及它们之间的一切。",
|
||||||
body:
|
subtitle: "切换 workspace 视图,掌握全貌。",
|
||||||
"这段 walkthrough 使用真实模拟器项目截图,展示 Issue 上下文、逐字输入感的评论草稿,以及一条轻快的 funk style 背景乐。",
|
items: [
|
||||||
|
{ title: "Projects", subtitle: "浏览项目和进度追踪" },
|
||||||
|
{ title: "项目详情", subtitle: "任务统计、状态字段和资源入口" },
|
||||||
|
{ title: "Agents", subtitle: "查看 agent 状态和最近活动" },
|
||||||
|
{ title: "Runtimes", subtitle: "检查运行时可用性和版本" },
|
||||||
|
{ title: "Skills", subtitle: "浏览 workspace agent 技能库" },
|
||||||
|
{ title: "技能详情", subtitle: "查看技能结构和文档" },
|
||||||
|
{ title: "Settings", subtitle: "账号、工作区和偏好控制" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
download: {
|
video: { kicker: "产品演示", title: "30 秒看完核心流程。", body: "快速演示 inbox 通知、issues 看板、上下文探索、评论回复、@ 提及和 workspace 视图切换。" },
|
||||||
kicker: "GitHub APK",
|
download: { kicker: "GitHub APK", title: "从 GitHub Releases 安装。", package: "Package", api: "Service API", sha: "APK SHA256", latest: "下载APK包", source: "源代码", star: "喜欢我们的 app?请在 GitHub 上给项目点个 Star ⭐,这是对我们作者最大的支持。" },
|
||||||
title: "从 GitHub Releases 安装。",
|
trust: {
|
||||||
package: "包名",
|
kicker: "信任与隐私",
|
||||||
api: "服务 API",
|
items: [
|
||||||
sha: "APK SHA256",
|
"社区维护的独立项目;与 Multica maintainers 无隶属关系,无背书。",
|
||||||
latest: "最新 Release",
|
"作为面向 Multica 用户的独立 Android app 发布,明确标注产品边界。",
|
||||||
source: "源码仓库",
|
"GPLv3 开源许可证 —— 可以查看、fork 或贡献。",
|
||||||
},
|
"不收集聊天内容、文件内容、邮箱、tokens 或精确位置信息。",
|
||||||
trustKicker: "信任与隐私",
|
],
|
||||||
trustItems: [
|
|
||||||
"这是独立 companion app,不声明拥有 Multica 项目,也不暗示维护者背书。",
|
|
||||||
"作为独立 Android App 发布给 Multica 用户,同时明确署名和产品边界。",
|
|
||||||
"公开 Android 版本里的 analytics 需要用户同意,并且仅限 app_open 事件。",
|
|
||||||
"不会有意上报聊天内容、文件内容、邮箱、token 或精确位置。",
|
|
||||||
],
|
|
||||||
footer: {
|
|
||||||
tagline: "面向 Multica 用户的独立 Android companion app,通过 GitHub 分发 APK。",
|
|
||||||
multica: "Multica 网站",
|
|
||||||
github: "GitHub",
|
|
||||||
download: "下载 APK",
|
|
||||||
},
|
},
|
||||||
|
footer: { tagline: "面向 Multica 用户的独立 Android companion app。GitHub 分发 APK。", multica: "Multica 官网", github: "GitHub", download: "下载 APK" },
|
||||||
},
|
},
|
||||||
} satisfies Record<Locale, unknown>;
|
};
|
||||||
|
|
||||||
function fade(reducedMotion: boolean | null, delay = 0): MotionProps {
|
const JOURNEY_STEPS = 9;
|
||||||
if (reducedMotion) {
|
const TYPING_TEXT = "Looks good. I'll ask the agent to check the failing case and post a follow-up.";
|
||||||
return {};
|
const MENTION_ITEMS = [
|
||||||
}
|
{ name: "codex", color: "#1647ff" },
|
||||||
return {
|
{ name: "Opus", color: "#7c3aed" },
|
||||||
initial: { opacity: 0, y: 24 },
|
{ name: "RollieOC", color: "#059669" },
|
||||||
animate: { opacity: 1, y: 0 },
|
];
|
||||||
transition: { duration: 0.7, delay, ease: "easeOut" },
|
|
||||||
};
|
const WORKSPACE_SCREENSHOTS = [projectsImg, projectDetailImg, agentsImg, runtimesImg, skillsImg, skillDetailImg, settingsImg];
|
||||||
|
|
||||||
|
const fade = (reduced: boolean, delay = 0) => ({
|
||||||
|
initial: reduced ? {} : { opacity: 0, y: 18 },
|
||||||
|
whileInView: { opacity: 1, y: 0 },
|
||||||
|
viewport: { once: true, margin: "-60px" },
|
||||||
|
transition: { duration: 0.65, delay, ease: [0.25, 0.1, 0.25, 1] as const },
|
||||||
|
});
|
||||||
|
|
||||||
|
const reveal = (reduced: boolean) => ({
|
||||||
|
initial: reduced ? {} : { opacity: 0, y: 20 },
|
||||||
|
whileInView: { opacity: 1, y: 0 },
|
||||||
|
viewport: { once: true, margin: "-80px" },
|
||||||
|
transition: { duration: 0.7, ease: [0.25, 0.1, 0.25, 1] as const },
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ── PhoneDemo ── */
|
||||||
|
function PhoneDemo({
|
||||||
|
src,
|
||||||
|
reduced,
|
||||||
|
zoomScale = 1,
|
||||||
|
zoomX = 0,
|
||||||
|
zoomY = 0,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
reduced: boolean;
|
||||||
|
zoomScale?: number;
|
||||||
|
zoomX?: number;
|
||||||
|
zoomY?: number;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="phone-shell-compact">
|
||||||
|
<div className="speaker" />
|
||||||
|
<div className="phone-screen-compact">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.img
|
||||||
|
key={src}
|
||||||
|
src={src}
|
||||||
|
alt="App screenshot"
|
||||||
|
initial={reduced ? {} : { opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
scale: zoomScale,
|
||||||
|
x: zoomX,
|
||||||
|
y: zoomY,
|
||||||
|
}}
|
||||||
|
exit={reduced ? {} : { opacity: 0 }}
|
||||||
|
transition={{ duration: reduced ? 0.1 : 0.4, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||||
|
/>
|
||||||
|
</AnimatePresence>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reveal(reducedMotion: boolean | null): MotionProps {
|
/* ── Typewriter effect ── */
|
||||||
if (reducedMotion) {
|
function TypewriterEffect({ active, text, reduced }: { active: boolean; text: string; reduced: boolean }) {
|
||||||
return {};
|
const charCount = useMotionValue(0);
|
||||||
}
|
const chars = useMemo(() => text.split(""), [text]);
|
||||||
return {
|
const charRefs = useRef<(HTMLSpanElement | null)[]>([]);
|
||||||
initial: { opacity: 0, y: 28 },
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
whileInView: { opacity: 1, y: 0 },
|
|
||||||
viewport: { once: true, margin: "-80px" },
|
// Imperatively update char opacity from motion value (no hooks in loops)
|
||||||
transition: { duration: 0.7, ease: "easeOut" },
|
useEffect(() => {
|
||||||
};
|
const unsubscribe = charCount.on("change", (v) => {
|
||||||
}
|
const count = Math.round(v);
|
||||||
|
charRefs.current.forEach((el, i) => {
|
||||||
|
if (el) el.style.opacity = i < count ? "1" : "0";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
}, [charCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
|
if (!active) { charCount.set(0); charRefs.current.forEach((el) => { if (el) el.style.opacity = "0"; }); return; }
|
||||||
|
if (reduced) {
|
||||||
|
charCount.set(chars.length);
|
||||||
|
charRefs.current.forEach((el, i) => { if (el) el.style.opacity = i < chars.length ? "1" : "0"; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let i = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
i++;
|
||||||
|
charCount.set(i);
|
||||||
|
if (i >= chars.length) clearInterval(interval);
|
||||||
|
}, 42);
|
||||||
|
intervalRef.current = interval;
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [active, reduced, chars.length]);
|
||||||
|
|
||||||
function PhoneMockup({ typing }: { typing: string }) {
|
|
||||||
const reducedMotion = useReducedMotion();
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="phone-wrap"
|
className="typing-line"
|
||||||
animate={reducedMotion ? {} : { y: [0, -10, 0], rotate: [-1.5, -0.6, -1.5] }}
|
animate={active ? { opacity: 1 } : { opacity: 0 }}
|
||||||
transition={reducedMotion ? {} : { duration: 7, repeat: Infinity, ease: "easeInOut" }}
|
transition={{ duration: 0.3 }}
|
||||||
>
|
>
|
||||||
<div className="phone-shell">
|
{chars.map((char, i) => (
|
||||||
<div className="speaker" />
|
<span
|
||||||
<div className="phone-screen">
|
key={i}
|
||||||
<div className="phone-screen-shot" aria-label="Multi-Casual Android issue detail screen" />
|
ref={(el) => { charRefs.current[i] = el; }}
|
||||||
<motion.div
|
style={{ opacity: 0 }}
|
||||||
className="typing-card"
|
>
|
||||||
initial={reducedMotion ? false : { opacity: 0, y: 14, scale: 0.98 }}
|
{char === " " ? "\u00A0" : char}
|
||||||
animate={reducedMotion ? { opacity: 1 } : { opacity: 1, y: 0, scale: 1 }}
|
</span>
|
||||||
transition={{ delay: 1.05, duration: 0.55 }}
|
))}
|
||||||
>
|
<motion.span
|
||||||
<span className="typing-dot" />
|
className="typing-cursor"
|
||||||
{typing}
|
animate={active ? { opacity: [1, 0.18] } : { opacity: 0 }}
|
||||||
</motion.div>
|
transition={active ? { duration: 0.64, repeat: Infinity, repeatType: "reverse" } : {}}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div className="tap-ring" aria-hidden="true" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── PhoneMockup (hero) ── */
|
||||||
|
function PhoneMockup({ reduced }: { reduced: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className="hero-phone-stack">
|
||||||
|
<motion.img
|
||||||
|
src={inboxImg} alt="Inbox preview"
|
||||||
|
initial={reduced ? {} : { opacity: 0, x: 60, rotate: -14 }}
|
||||||
|
whileInView={{ opacity: 0.72, x: 0, rotate: -8 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.85, delay: 0.2, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||||
|
/>
|
||||||
|
<motion.img
|
||||||
|
src={issuesListImg} alt="Issues list preview"
|
||||||
|
initial={reduced ? {} : { opacity: 0, x: 80, rotate: 8 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0, rotate: 4 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.85, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||||
|
/>
|
||||||
|
<motion.img
|
||||||
|
src={issueDetailImg} alt="Issue detail preview"
|
||||||
|
initial={reduced ? {} : { opacity: 0, x: 60, rotate: 14 }}
|
||||||
|
whileInView={{ opacity: 0.86, x: 0, rotate: 10 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.85, delay: 0.4, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main App ── */
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const reducedMotion = useReducedMotion();
|
const reduced = useReducedMotion() ?? false;
|
||||||
const [locale, setLocale] = useState<Locale>("en");
|
const [lang, setLang] = useState<Locale>("en");
|
||||||
const t = copy[locale];
|
const [step, setStep] = useState(0);
|
||||||
|
const [activeWs, setActiveWs] = useState(0);
|
||||||
|
const [hasInteracted, setHasInteracted] = useState(false);
|
||||||
|
const completedRef = useRef<Set<number>>(new Set());
|
||||||
|
const journeyRef = useRef<HTMLDivElement>(null);
|
||||||
|
const hasAutoPlayedRef = useRef(false);
|
||||||
|
const t = copy[lang];
|
||||||
|
|
||||||
|
// Auto-play journey on first scroll into view
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasInteracted || hasAutoPlayedRef.current) return;
|
||||||
|
const el = journeyRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting && entry.intersectionRatio > 0.3 && !hasAutoPlayedRef.current) {
|
||||||
|
hasAutoPlayedRef.current = true;
|
||||||
|
let currentStep = 0;
|
||||||
|
const advance = () => {
|
||||||
|
currentStep++;
|
||||||
|
if (currentStep < JOURNEY_STEPS) {
|
||||||
|
setStep(currentStep);
|
||||||
|
setTimeout(advance, 2200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(advance, 2200);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.3 }
|
||||||
|
);
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [hasInteracted]);
|
||||||
|
|
||||||
|
const handleStepClick = useCallback((index: number) => {
|
||||||
|
setHasInteracted(true);
|
||||||
|
setStep(index);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWorkspaceClick = useCallback((index: number) => {
|
||||||
|
setActiveWs(index);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Phone screen source based on journey step
|
||||||
|
const phoneScreenSrc = useMemo(() => {
|
||||||
|
if (step <= 1) return step === 0 ? inboxImg : issuesListImg;
|
||||||
|
if (step === 2) return issuesBoardImg;
|
||||||
|
if (step <= 5) return step === 3 ? issueDetailImg : issueSubImg;
|
||||||
|
if (step === 6) return commentImg;
|
||||||
|
if (step === 7) return mentionImg;
|
||||||
|
return projectsImg;
|
||||||
|
}, [step]);
|
||||||
|
|
||||||
|
// Focus zoom transform for step 4 (context explore)
|
||||||
|
const focusZones = [
|
||||||
|
{ scale: 1.6, x: 0, y: -120 }, // title/status
|
||||||
|
{ scale: 1.45, x: 0, y: -40 }, // body
|
||||||
|
{ scale: 1.5, x: 0, y: 80 }, // subtasks/comments
|
||||||
|
];
|
||||||
|
const focusIdx = step === 4 ? 0 : -1; // simple: show first zone; cycle handled by auto-play
|
||||||
|
const zoom = step === 4 ? focusZones[focusIdx] || focusZones[0] : { scale: 1, x: 0, y: 0 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main lang={locale === "zh" ? "zh-CN" : "en"}>
|
<main lang={lang === "zh" ? "zh-CN" : "en"}>
|
||||||
<section className="hero section-pad">
|
{/* Hero */}
|
||||||
|
<section className="hero">
|
||||||
<div className="hero-bg" />
|
<div className="hero-bg" />
|
||||||
<nav className="topbar" aria-label="Primary navigation">
|
<motion.nav className="topbar" {...fade(reduced, 0)}>
|
||||||
<a className="brand" href="#top" aria-label="Multi-Casual Android home">
|
<div className="brand">
|
||||||
<span className="brand-mark">M</span>
|
<img src={logoImg} alt="Multi-Casual" className="brand-mark" />
|
||||||
<span>{t.nav.brand}</span>
|
<span>{t.nav.brand}</span>
|
||||||
</a>
|
</div>
|
||||||
<div className="nav-links">
|
<div className="nav-links">
|
||||||
<a href="#download">{t.nav.download}</a>
|
<a href="#download">{t.nav.download}</a>
|
||||||
<a href="#trust">{t.nav.trust}</a>
|
<a href="#trust">{t.nav.trust}</a>
|
||||||
<a href="#relationship">{t.nav.relationship}</a>
|
<button className="language-toggle" onClick={() => setLang((l) => (l === "en" ? "zh" : "en"))}>
|
||||||
<button className="language-toggle" type="button" onClick={() => setLocale(locale === "en" ? "zh" : "en")}>
|
|
||||||
{t.nav.language}
|
{t.nav.language}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</motion.nav>
|
||||||
|
<div className="hero-grid">
|
||||||
<div id="top" className="hero-grid">
|
<div>
|
||||||
<div className="hero-copy">
|
<motion.p className="eyebrow" {...fade(reduced, 0.04)}>{t.hero.eyebrow}</motion.p>
|
||||||
<motion.p className="eyebrow" {...fade(reducedMotion, 0.05)}>
|
<motion.h1 {...fade(reduced, 0.14)}>{t.hero.title}</motion.h1>
|
||||||
{t.hero.eyebrow}
|
<motion.p className="hero-subtitle" {...fade(reduced, 0.24)}>{t.hero.subtitle}</motion.p>
|
||||||
</motion.p>
|
<motion.div className="cta-row" {...fade(reduced, 0.34)}>
|
||||||
<motion.h1 {...fade(reducedMotion, 0.14)}>{t.hero.title}</motion.h1>
|
<motion.a className="button primary" href="#download" whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}>{t.hero.download}</motion.a>
|
||||||
<motion.p className="hero-subtitle" {...fade(reducedMotion, 0.24)}>
|
<motion.a className="button secondary" href={release.githubUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" style={{verticalAlign: "middle", marginRight: 6}}><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>{t.hero.github}</motion.a>
|
||||||
{t.hero.subtitle}
|
<motion.a className="button ghost" href={release.multicaUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}>{t.hero.multica}</motion.a>
|
||||||
</motion.p>
|
|
||||||
<motion.div className="cta-row" {...fade(reducedMotion, 0.34)}>
|
|
||||||
<motion.a className="button primary" href={release.releasesUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}>
|
|
||||||
{t.hero.download}
|
|
||||||
</motion.a>
|
|
||||||
<motion.a className="button secondary" href={release.githubUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}>
|
|
||||||
{t.hero.github}
|
|
||||||
</motion.a>
|
|
||||||
<motion.a className="button ghost" href={release.multicaUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}>
|
|
||||||
{t.hero.multica}
|
|
||||||
</motion.a>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
<motion.div {...fade(reducedMotion, 0.18)}>
|
<PhoneMockup reduced={reduced} />
|
||||||
<PhoneMockup typing={t.hero.typing} />
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<motion.section id="relationship" className="relationship section-pad" {...reveal(reducedMotion)}>
|
{/* Relationship */}
|
||||||
|
<motion.section id="relationship" className="relationship section-pad" {...reveal(reduced)}>
|
||||||
<div className="section-kicker">{t.relationship.kicker}</div>
|
<div className="section-kicker">{t.relationship.kicker}</div>
|
||||||
<div className="two-col">
|
<div className="relationship-card">
|
||||||
<h2>{t.relationship.title}</h2>
|
<h2>{t.relationship.title}</h2>
|
||||||
<p>{t.relationship.body}</p>
|
<p>{t.relationship.body1}</p>
|
||||||
|
<p>{t.relationship.body2}</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/multica-ai/multica/issues/2285#issuecomment-4414513596" target="_blank" rel="noopener noreferrer">
|
||||||
|
View the discussion on GitHub →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
|
|
||||||
<section className="features section-pad">
|
{/* Interactive Product Journey */}
|
||||||
<div className="section-kicker">{t.featuresKicker}</div>
|
<section ref={journeyRef} className="journey section-pad">
|
||||||
<div className="feature-grid">
|
<motion.div className="journey-header" {...reveal(reduced)}>
|
||||||
{t.features.map((feature, index) => (
|
<div className="section-kicker">{t.journey.kicker}</div>
|
||||||
<motion.article key={feature.title} className="feature-card" {...reveal(reducedMotion)} transition={{ duration: 0.7, delay: index * 0.08 }}>
|
<h2>{t.journey.title}</h2>
|
||||||
<span>{String(index + 1).padStart(2, "0")}</span>
|
<p>{t.journey.subtitle}</p>
|
||||||
<h3>{feature.title}</h3>
|
</motion.div>
|
||||||
<p>{feature.body}</p>
|
|
||||||
</motion.article>
|
<div className="journey-layout">
|
||||||
|
<div className="journey-steps">
|
||||||
|
{t.journey.steps.map((s, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className={`journey-step ${step === i ? "journey-step-active" : ""} ${i < step ? "journey-step-completed" : ""}`}
|
||||||
|
onClick={() => handleStepClick(i)}
|
||||||
|
initial={reduced ? {} : { opacity: 0, x: -16 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: i * 0.06 }}
|
||||||
|
>
|
||||||
|
<div className="journey-step-number">{i < step ? "✓" : i + 1}</div>
|
||||||
|
<div className="journey-step-content">
|
||||||
|
<h3>{s.title}</h3>
|
||||||
|
<p>{s.subtitle}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="journey-phone-area">
|
||||||
|
<div className="journey-phone-container">
|
||||||
|
<PhoneDemo
|
||||||
|
src={phoneScreenSrc}
|
||||||
|
reduced={reduced}
|
||||||
|
zoomScale={step === 4 ? zoom.scale : 1}
|
||||||
|
zoomX={step === 4 ? zoom.x : 0}
|
||||||
|
zoomY={step === 4 ? zoom.y : 0}
|
||||||
|
>
|
||||||
|
{/* Hand + tap ripple for interactive steps */}
|
||||||
|
{step >= 1 && step <= 3 && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="journey-hand"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
style={{ top: step === 1 ? "16%" : step === 2 ? "8%" : "35%", right: step === 2 ? "18%" : "22%" }}
|
||||||
|
>👆</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="journey-tap-ripple"
|
||||||
|
animate={{ scale: [1, 1.3, 1], opacity: [0.8, 0.3, 0.8] }}
|
||||||
|
transition={{ duration: 1.2, repeat: Infinity }}
|
||||||
|
style={{ top: step === 1 ? "18%" : step === 2 ? "10%" : "37%", right: step === 2 ? "14%" : "18%" }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Typing effect for step 6 */}
|
||||||
|
{step === 6 && <TypewriterEffect active text={TYPING_TEXT} reduced={reduced} />}
|
||||||
|
|
||||||
|
{/* Mention picker overlay for step 7 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{step === 7 && (
|
||||||
|
<motion.div
|
||||||
|
className="mention-overlay"
|
||||||
|
initial={{ y: 60, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 60, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.35, ease: [0.25, 0.1, 0.25, 1] as const }}
|
||||||
|
>
|
||||||
|
<div className="mention-search">
|
||||||
|
<span>@</span> cod
|
||||||
|
</div>
|
||||||
|
<div className="mention-list">
|
||||||
|
{MENTION_ITEMS.map((m, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={m.name}
|
||||||
|
className={`mention-item ${i === 0 ? "mention-item-active" : ""}`}
|
||||||
|
initial={{ opacity: 0, x: -12 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.15 + i * 0.1 }}
|
||||||
|
>
|
||||||
|
<div className="mention-avatar" style={{ background: m.color }}>{m.name[0].toUpperCase()}</div>
|
||||||
|
<div>
|
||||||
|
<div className="mention-name">{m.name}</div>
|
||||||
|
<div className="mention-role">{i === 0 ? "agent" : i === 1 ? "agent" : "squad"}</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Workspace chips overlay for step 8 */}
|
||||||
|
{step === 8 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
style={{ position: "absolute", bottom: 8, left: 8, right: 8, zIndex: 6, display: "flex", gap: 4, justifyContent: "center", flexWrap: "wrap" }}
|
||||||
|
>
|
||||||
|
{["Projects", "Agents", "Runtimes", "Skills", "Settings"].map((label, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={label}
|
||||||
|
className="workspace-chip"
|
||||||
|
style={{ fontSize: 10, padding: "4px 8px", borderRadius: 999 }}
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: 0.1 + i * 0.08 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</PhoneDemo>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Workspace Assets Grid */}
|
||||||
|
<motion.section className="workspace-section section-pad" {...reveal(reduced)}>
|
||||||
|
<div className="workspace-header">
|
||||||
|
<div className="section-kicker">{t.workspace.kicker}</div>
|
||||||
|
<h2>{t.workspace.title}</h2>
|
||||||
|
<p>{t.workspace.subtitle}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="workspace-chips">
|
||||||
|
{t.workspace.items.map((item, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className={`workspace-chip ${activeWs === i ? "workspace-chip-active" : ""}`}
|
||||||
|
onClick={() => handleWorkspaceClick(i)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<motion.section className="demo section-pad" {...reveal(reducedMotion)}>
|
<div className="workspace-phone-wrap">
|
||||||
<div className="section-kicker">{t.demo.kicker}</div>
|
<AnimatePresence mode="wait">
|
||||||
<div className="demo-grid">
|
<motion.div
|
||||||
<div>
|
key={activeWs}
|
||||||
<h2>{t.demo.title}</h2>
|
initial={reduced ? {} : { opacity: 0, scale: 0.96 }}
|
||||||
<p>{t.demo.body}</p>
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
</div>
|
exit={reduced ? {} : { opacity: 0, scale: 0.96 }}
|
||||||
<div className="screenshot-stack" aria-label="Issue comment flow screenshots">
|
transition={{ duration: 0.35 }}
|
||||||
<img className="shot back" src={issueDetail} alt="Issue detail screenshot" />
|
>
|
||||||
<img className="shot front" src={commentFocused} alt="Focused issue comment input screenshot" />
|
<PhoneDemo src={WORKSPACE_SCREENSHOTS[activeWs]} reduced={reduced} />
|
||||||
</div>
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<motion.div className="workspace-card-grid" {...reveal(reduced)}>
|
||||||
|
{t.workspace.items.map((item, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className={`workspace-card ${activeWs === i ? "workspace-card-active" : ""}`}
|
||||||
|
onClick={() => handleWorkspaceClick(i)}
|
||||||
|
initial={reduced ? {} : { opacity: 0, y: 16 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: i * 0.06 }}
|
||||||
|
>
|
||||||
|
<img src={WORKSPACE_SCREENSHOTS[i]} alt={item.title} className="workspace-card-thumb" />
|
||||||
|
<div className="workspace-card-label">
|
||||||
|
<strong>{item.title}</strong>
|
||||||
|
<span>{item.subtitle}</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
|
|
||||||
<motion.section className="video-demo section-pad" {...reveal(reducedMotion)}>
|
{/* Video Demo */}
|
||||||
|
<motion.section className="video-demo section-pad" {...reveal(reduced)}>
|
||||||
<div className="section-kicker">{t.video.kicker}</div>
|
<div className="section-kicker">{t.video.kicker}</div>
|
||||||
<div className="video-copy">
|
<div className="video-copy">
|
||||||
<h2>{t.video.title}</h2>
|
<h2>{t.video.title}</h2>
|
||||||
<p>{t.video.body}</p>
|
<p>{t.video.body}</p>
|
||||||
</div>
|
</div>
|
||||||
<video className="promo-video" src={promoVideo} controls playsInline preload="metadata" poster={commentFocused} />
|
<video className="promo-video" src={promoVideo} controls playsInline preload="metadata" />
|
||||||
</motion.section>
|
</motion.section>
|
||||||
|
|
||||||
|
{/* Download */}
|
||||||
<section id="download" className="download section-pad">
|
<section id="download" className="download section-pad">
|
||||||
<motion.div className="release-card" {...reveal(reducedMotion)}>
|
<motion.div className="release-card" {...reveal(reduced)}>
|
||||||
<div className="section-kicker">{t.download.kicker}</div>
|
<div className="section-kicker">{t.download.kicker}</div>
|
||||||
<h2>{t.download.title}</h2>
|
<h2>{t.download.title}</h2>
|
||||||
<div className="release-grid">
|
<div className="release-grid">
|
||||||
@@ -334,14 +641,21 @@ export default function App() {
|
|||||||
<a className="button primary" href={release.releasesUrl}>{t.download.latest}</a>
|
<a className="button primary" href={release.releasesUrl}>{t.download.latest}</a>
|
||||||
<a className="button secondary" href={release.githubUrl}>{t.download.source}</a>
|
<a className="button secondary" href={release.githubUrl}>{t.download.source}</a>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="star-hint">
|
||||||
|
{lang === "en"
|
||||||
|
? <>Like the app? Give us a star on <a href={release.githubUrl} target="_blank" rel="noopener">GitHub</a> ⭐ — it means a lot to the maintainers.</>
|
||||||
|
: <>喜欢我们的 app?请在 <a href={release.githubUrl} target="_blank" rel="noopener">GitHub</a> 上给项目点个 Star ⭐,这是对我们作者最大的支持。</>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Trust */}
|
||||||
<section id="trust" className="trust section-pad">
|
<section id="trust" className="trust section-pad">
|
||||||
<div className="section-kicker">{t.trustKicker}</div>
|
<div className="section-kicker">{t.trust.kicker}</div>
|
||||||
<div className="trust-grid">
|
<div className="trust-grid">
|
||||||
{t.trustItems.map((item) => (
|
{t.trust.items.map((item) => (
|
||||||
<motion.div key={item} className="trust-item" {...reveal(reducedMotion)}>
|
<motion.div key={item} className="trust-item" {...reveal(reduced)}>
|
||||||
<span className="check">✓</span>
|
<span className="check">✓</span>
|
||||||
<p>{item}</p>
|
<p>{item}</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -349,6 +663,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
<footer className="footer section-pad">
|
<footer className="footer section-pad">
|
||||||
<div>
|
<div>
|
||||||
<strong>Multi-Casual Android</strong>
|
<strong>Multi-Casual Android</strong>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 397 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
Before Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 450 KiB |
|
After Width: | Height: | Size: 347 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 267 KiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 386 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 284 KiB |
@@ -20,10 +20,11 @@ html { scroll-behavior: smooth; }
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
|
overflow-x: hidden;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 80% 5%, rgba(255, 228, 92, 0.32), transparent 30%),
|
radial-gradient(circle at 82% 5%, rgba(255, 228, 92, 0.4), transparent 31%),
|
||||||
radial-gradient(circle at 15% 25%, rgba(22, 71, 255, 0.14), transparent 28%),
|
radial-gradient(circle at 10% 30%, rgba(22, 71, 255, 0.16), transparent 30%),
|
||||||
var(--paper);
|
linear-gradient(180deg, #fbfcff 0%, #e8eeff 55%, #d4dff9 85%, #c4d4f5 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
body, button, input { font: inherit; }
|
body, button, input { font: inherit; }
|
||||||
@@ -39,6 +40,8 @@ a { color: inherit; text-decoration: none; }
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-top: 26px;
|
padding-top: 26px;
|
||||||
|
padding-left: clamp(16px, 5vw, 76px);
|
||||||
|
padding-right: clamp(16px, 5vw, 76px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-bg {
|
.hero-bg {
|
||||||
@@ -76,12 +79,9 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.brand-mark {
|
.brand-mark {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
display: grid;
|
object-fit: cover;
|
||||||
place-items: center;
|
display: block;
|
||||||
color: white;
|
|
||||||
background: var(--blue);
|
|
||||||
box-shadow: 0 8px 18px rgba(22, 71, 255, 0.32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a {
|
.nav-links a {
|
||||||
@@ -160,7 +160,7 @@ h3 {
|
|||||||
letter-spacing: -0.035em;
|
letter-spacing: -0.035em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-subtitle, .relationship p, .demo p {
|
.hero-subtitle, .relationship p, .demo p, .journey-desc {
|
||||||
max-width: 650px;
|
max-width: 650px;
|
||||||
color: #343b4a;
|
color: #343b4a;
|
||||||
font-size: clamp(18px, 2vw, 23px);
|
font-size: clamp(18px, 2vw, 23px);
|
||||||
@@ -186,6 +186,7 @@ h3 {
|
|||||||
.button.secondary { background: white; border-color: var(--line); }
|
.button.secondary { background: white; border-color: var(--line); }
|
||||||
.button.ghost { color: var(--muted); background: rgba(255, 255, 255, 0.54); border-color: var(--line); }
|
.button.ghost { color: var(--muted); background: rgba(255, 255, 255, 0.54); border-color: var(--line); }
|
||||||
|
|
||||||
|
/* Phone mockup */
|
||||||
.phone-wrap {
|
.phone-wrap {
|
||||||
width: min(390px, 82vw);
|
width: min(390px, 82vw);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -193,6 +194,28 @@ h3 {
|
|||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-phone-stack {
|
||||||
|
min-height: 720px;
|
||||||
|
position: relative;
|
||||||
|
perspective: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-phone-stack img {
|
||||||
|
position: absolute;
|
||||||
|
width: min(310px, 54vw);
|
||||||
|
height: 676px;
|
||||||
|
border-radius: 46px;
|
||||||
|
border: 12px solid #11141d;
|
||||||
|
background: white;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top center;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-phone-stack img:nth-child(1) { left: 2%; top: 48px; transform: rotate(-8deg) translateZ(-20px); }
|
||||||
|
.hero-phone-stack img:nth-child(2) { left: 28%; top: 0; transform: rotate(4deg) translateZ(40px); }
|
||||||
|
.hero-phone-stack img:nth-child(3) { right: 0; top: 132px; transform: rotate(10deg) translateZ(10px); }
|
||||||
|
|
||||||
.phone-shell {
|
.phone-shell {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
@@ -201,6 +224,14 @@ h3 {
|
|||||||
box-shadow: var(--shadow), inset 0 0 0 2px rgba(255,255,255,0.1);
|
box-shadow: var(--shadow), inset 0 0 0 2px rgba(255,255,255,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phone-shell-compact {
|
||||||
|
position: relative;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 48px;
|
||||||
|
background: #11141d;
|
||||||
|
box-shadow: 0 20px 56px rgba(16, 18, 24, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
.speaker {
|
.speaker {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 18px;
|
top: 18px;
|
||||||
@@ -221,43 +252,33 @@ h3 {
|
|||||||
aspect-ratio: 9 / 19.7;
|
aspect-ratio: 9 / 19.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phone-screen img { width: 100%; height: 100%; object-fit: cover; object-position: top center; display: block; }
|
.phone-screen-compact {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 36px;
|
||||||
|
background: white;
|
||||||
|
aspect-ratio: 9 / 19.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen img,
|
||||||
|
.phone-screen-compact img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.phone-screen-shot {
|
.phone-screen-shot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-image: url("./assets/screenshots/02-issue-detail.png");
|
width: 100%;
|
||||||
background-size: cover;
|
height: 100%;
|
||||||
background-position: top center;
|
object-fit: cover;
|
||||||
background-repeat: no-repeat;
|
object-position: top center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typing-card {
|
|
||||||
position: absolute;
|
|
||||||
left: 22px;
|
|
||||||
right: 22px;
|
|
||||||
bottom: 22px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 20px;
|
|
||||||
color: #142142;
|
|
||||||
background: rgba(255,255,255,0.92);
|
|
||||||
border: 1px solid rgba(22, 71, 255, 0.22);
|
|
||||||
box-shadow: 0 20px 38px rgba(16, 18, 24, 0.18);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.typing-dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
flex: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--yellow);
|
|
||||||
box-shadow: 0 0 0 5px rgba(255, 228, 92, 0.26);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tap-ring {
|
.tap-ring {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -271,6 +292,7 @@ h3 {
|
|||||||
box-shadow: 0 0 0 16px rgba(255, 228, 92, 0.22);
|
box-shadow: 0 0 0 16px rgba(255, 228, 92, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shared content sections */
|
||||||
.relationship, .features, .demo, .video-demo, .download, .trust, .footer {
|
.relationship, .features, .demo, .video-demo, .download, .trust, .footer {
|
||||||
width: min(1160px, calc(100% - 44px));
|
width: min(1160px, calc(100% - 44px));
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -285,6 +307,356 @@ h3 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Relationship section */
|
||||||
|
.relationship-card {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 30px;
|
||||||
|
background: rgba(255,255,255,0.76);
|
||||||
|
box-shadow: 0 18px 48px rgba(16,18,24,0.08);
|
||||||
|
padding: clamp(28px, 5vw, 48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-card p {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: clamp(16px, 1.8vw, 20px);
|
||||||
|
line-height: 1.65;
|
||||||
|
color: #343b4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-card p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.relationship-card a {
|
||||||
|
color: var(--blue);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: rgba(22, 71, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.relationship-card a:hover { text-decoration-color: var(--blue); }
|
||||||
|
|
||||||
|
/* Interactive product journey */
|
||||||
|
.journey {
|
||||||
|
width: min(1160px, calc(100% - 44px));
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-header h2 {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-header p {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(280px, 440px);
|
||||||
|
gap: clamp(24px, 5vw, 64px);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-steps {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step {
|
||||||
|
display: flex;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: rgba(255,255,255,0.68);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step:hover {
|
||||||
|
background: rgba(255,255,255,0.92);
|
||||||
|
border-color: rgba(22, 71, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step-active {
|
||||||
|
background: white;
|
||||||
|
border-color: var(--blue);
|
||||||
|
box-shadow: 0 12px 36px rgba(22, 71, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step-completed {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step-number {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(22, 71, 255, 0.08);
|
||||||
|
color: var(--blue);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: none;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step-active .journey-step-number {
|
||||||
|
background: var(--blue);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step-completed .journey-step-number {
|
||||||
|
background: var(--blue);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step-content h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-step-content p {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-phone-area {
|
||||||
|
position: sticky;
|
||||||
|
top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-phone-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Journey phone overlays */
|
||||||
|
.journey-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 6;
|
||||||
|
border-radius: 36px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-hand {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 8;
|
||||||
|
font-size: 28px;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.journey-tap-ripple {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 7;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid var(--blue);
|
||||||
|
background: rgba(255, 228, 92, 0.55);
|
||||||
|
box-shadow: 0 0 0 12px rgba(255, 228, 92, 0.18);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus zoom */
|
||||||
|
.focus-zoom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 3px solid var(--blue);
|
||||||
|
background: rgba(22, 71, 255, 0.06);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workspace assets grid */
|
||||||
|
.workspace-section {
|
||||||
|
width: min(1160px, calc(100% - 44px));
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header h2 {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-header p {
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chips {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip {
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 850;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: rgba(255,255,255,0.72);
|
||||||
|
color: var(--muted);
|
||||||
|
transition: background 0.18s, color 0.18s, border-color 0.18s, box-shadow 0.18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip:hover {
|
||||||
|
background: white;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-chip-active {
|
||||||
|
background: var(--blue);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--blue);
|
||||||
|
box-shadow: 0 10px 24px rgba(22, 71, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-phone-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-phone-wrap .phone-shell-compact {
|
||||||
|
width: min(260px, 60vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mention picker overlay */
|
||||||
|
.mention-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(255,255,255,0.97);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
border-radius: 24px 24px 36px 36px;
|
||||||
|
padding: 16px 14px;
|
||||||
|
box-shadow: 0 -12px 36px rgba(16, 18, 24, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #f4f6fa;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-search span {
|
||||||
|
color: var(--blue);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item-active {
|
||||||
|
background: rgba(22, 71, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: white;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-role {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-highlight {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 2px solid var(--blue);
|
||||||
|
background: rgba(22, 71, 255, 0.06);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typing demo */
|
||||||
|
.typing-line {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: pre;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-cursor {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--blue);
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Features grid (legacy, kept for reference) */
|
||||||
.feature-grid, .trust-grid, .release-grid {
|
.feature-grid, .trust-grid, .release-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -346,7 +718,7 @@ h3 {
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.promo-video { object-fit: cover; }
|
.promo-video { object-fit: cover; z-index: 1; position: relative; }
|
||||||
|
|
||||||
.video-placeholder {
|
.video-placeholder {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -426,16 +798,33 @@ pre {
|
|||||||
.footer strong { color: var(--ink); }
|
.footer strong { color: var(--ink); }
|
||||||
.footer-links { display: flex; gap: 18px; flex-wrap: wrap; font-weight: 800; color: var(--ink); }
|
.footer-links { display: flex; gap: 18px; flex-wrap: wrap; font-weight: 800; color: var(--ink); }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.topbar { border-radius: 24px; align-items: flex-start; flex-direction: column; }
|
.topbar { border-radius: 24px; align-items: flex-start; flex-direction: column; }
|
||||||
.nav-links { flex-wrap: wrap; }
|
.nav-links { flex-wrap: wrap; }
|
||||||
.hero-grid, .two-col, .demo-grid, .video-copy, .feature-grid, .trust-grid, .release-grid { grid-template-columns: 1fr; }
|
.hero-grid, .two-col, .demo-grid, .video-copy, .feature-grid, .trust-grid, .release-grid { grid-template-columns: 1fr; }
|
||||||
|
.hero { padding-left: 16px; padding-right: 16px; padding-top: 20px; min-height: auto; }
|
||||||
|
.hero-grid { width: 100%; }
|
||||||
|
.hero-grid > div:first-child { padding-right: 0; }
|
||||||
|
.hero-phone-stack { min-height: 400px; overflow: visible; }
|
||||||
|
.hero-phone-stack img { height: 480px; width: min(240px, 42vw); border-radius: 36px; border-width: 10px; }
|
||||||
|
.hero-phone-stack img:nth-child(1) { left: -5%; top: 36px; transform: rotate(-8deg) translateZ(-20px); }
|
||||||
|
.hero-phone-stack img:nth-child(2) { left: 22%; top: 0; transform: rotate(4deg) translateZ(40px); }
|
||||||
|
.hero-phone-stack img:nth-child(3) { right: -5%; top: 100px; transform: rotate(10deg) translateZ(10px); }
|
||||||
|
.eyebrow { font-size: 11px; letter-spacing: 0.1em; }
|
||||||
|
h1 { font-size: clamp(32px, 8vw, 52px) !important; }
|
||||||
|
.hero-subtitle { font-size: 15px; }
|
||||||
|
.journey-layout { grid-template-columns: 1fr; }
|
||||||
|
.journey-phone-area { position: relative; top: 0; order: -1; }
|
||||||
|
.journey-phone-container { justify-content: center; }
|
||||||
|
.section-pad { padding: 48px 16px; }
|
||||||
.hash-cell { grid-column: auto; }
|
.hash-cell { grid-column: auto; }
|
||||||
.screenshot-stack { min-height: 680px; }
|
.screenshot-stack { min-height: 680px; }
|
||||||
.shot { width: min(300px, 74vw); }
|
.shot { width: min(300px, 74vw); }
|
||||||
.shot.back { right: auto; left: 4%; }
|
.shot.back { right: auto; left: 4%; }
|
||||||
.shot.front { right: 4%; top: 160px; }
|
.shot.front { right: 4%; top: 160px; }
|
||||||
.footer { flex-direction: column; }
|
.footer { flex-direction: column; }
|
||||||
|
.workspace-chips { justify-content: flex-start; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
@@ -445,3 +834,78 @@ pre {
|
|||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Workspace card grid */
|
||||||
|
.workspace-card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-card {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-card:hover {
|
||||||
|
background: white;
|
||||||
|
border-color: rgba(22, 71, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-card-active {
|
||||||
|
border-color: var(--blue);
|
||||||
|
box-shadow: 0 10px 28px rgba(22, 71, 255, 0.12);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-card-thumb {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 9 / 19.7;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top center;
|
||||||
|
display: block;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-card-label {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-card-label strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-card-label span {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.workspace-card-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-hint {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-hint a {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|||||||