Publish Xiaomi subsite without APK artifact

This commit is contained in:
Xisheng-Zhao
2026-06-26 17:30:32 +08:00
parent 1e4a88677f
commit 5800000268
30 changed files with 2881 additions and 1 deletions
+6 -1
View File
@@ -5,6 +5,7 @@ on:
branches: [main]
paths:
- 'site/**'
- 'site-internal/**'
- '.github/workflows/deploy-site.yml'
workflow_dispatch:
@@ -26,8 +27,12 @@ jobs:
with:
node-version: 20
cache: npm
cache-dependency-path: site/package-lock.json
cache-dependency-path: |
site/package-lock.json
site-internal/package-lock.json
- run: cd site && npm install && VITE_BASE=/multi-casual-android/ npm run build
- run: cd site-internal && npm install && VITE_BASE=/multi-casual-android/xiaomi/ npm run build
- run: mkdir -p site/dist/xiaomi && cp -R site-internal/dist/. site/dist/xiaomi/
- uses: actions/upload-pages-artifact@v3
with:
path: site/dist
+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
.edgeone/
*.tsbuildinfo
public/downloads/*.apk
+78
View File
@@ -0,0 +1,78 @@
# site-internal — Multica Xiaomi internal landing page
A separate Vite + React landing page for the **Multica Xiaomi** internal
Android build. The public site at [`../site/`](../site/) targets the
Multi-Casual public release; this directory is a sibling project
published under the same GitHub Pages site at `/xiaomi/`.
## Visual scope
This page is a near-clone of the public site (`site/src/`) with three
intentional differences:
1. The "Relationship to Multica" section is removed.
2. The source link points to the private company GitLab project.
The signed APK download points to a separate artifact-only GitHub
release repository.
3. The iOS App button is left untouched and still points at the
public iOS repo — the same `github.com/park0er/multi-casual-ios-app`
source-of-truth is reused on purpose.
All screenshots, the promo video, the journey / workspace interactions,
the language toggle, and the layout are reused from `site/src/assets/`
so the visual stays in sync with the public site.
## Release metadata
`src/App.tsx` points the primary APK download button at the static
GitHub release asset:
```text
https://github.com/park0er/multica-xiaomi-apk/releases/download/v0.1.5-xiaomi/Multica-Xiaomi-v0.1.5.apk
```
The APK is copied from `app/build/outputs/apk/xiaomi/release/` into
`site-internal/public/downloads/` before deployment. Current metadata:
| Field | Value |
|---|---|
| `release.sha256` | `13e004472d33e66aa40aa7ebd58f75e22cfde8cce3a153729481f1be4d2d1414` |
| `release.gitlabUrl` | `https://git.n.xiaomi.com/zhaoxisheng/multica-xiaomi-android` |
| `release.releasesUrl` | `https://github.com/park0er/multica-xiaomi-apk/releases/download/v0.1.5-xiaomi/Multica-Xiaomi-v0.1.5.apk` |
Also confirm the `release.apiUrl` matches the staging or production
Xiaomi Multica endpoint you want this build to talk to
(`http://staging-multica.ad.xiaomi.srv` is the build.gradle default).
## Build
```bash
cd site-internal
npm install
npm run build
```
Output goes to `site-internal/dist/` and is gitignored.
## Deploy
The site is hosted as a subdirectory of the public GitHub Pages site:
```
https://park0er.github.io/multi-casual-android/xiaomi/
```
GitHub Actions builds the public site into the Pages root and this
internal Xiaomi site into `dist/xiaomi/` before deploying.
### Publish flow
1. Copy `site-internal/` into the public repo.
2. Commit and push the public repo.
3. GitHub Actions deploys both `site/` and `site-internal/`.
### EdgeOne status
Tencent EdgeOne hosting was attempted first but proved unreliable for
this internal distribution. The current source of truth for downloads is
GitHub Pages.
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Multica Xiaomi is the Xiaomi-flavor Android build of the Multica companion app, distributed internally via the company GitLab." />
<link rel="icon" href="./favicon.png" type="image/png" />
<title>Multica Xiaomi · Internal Build</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1001
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "multica-xiaomi-internal-site",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "vite --host 127.0.0.1",
"build": "tsc -b && vite build",
"preview": "vite preview --host 127.0.0.1"
},
"dependencies": {
"@vitejs/plugin-react": "latest",
"motion": "latest",
"react": "latest",
"react-dom": "latest",
"typescript": "latest",
"vite": "latest"
},
"devDependencies": {
"@types/react": "^19.2.16",
"@types/react-dom": "^19.2.3"
}
}
+77
View File
@@ -0,0 +1,77 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="refresh" content="0.8; url=https://park0er.github.io/multi-casual-android/xiaomi/" />
<title>跳转到 Multica Xiaomi</title>
<style>
:root {
color-scheme: light;
--ink: #101218;
--muted: #5b6472;
--blue: #1647ff;
--paper: #f6f8fc;
--card: #ffffff;
--line: rgba(16, 18, 24, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--paper);
color: var(--ink);
}
main {
width: min(520px, calc(100vw - 32px));
padding: 32px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--card);
box-shadow: 0 24px 70px rgba(16, 18, 24, 0.12);
}
h1 {
margin: 0 0 12px;
font-size: 26px;
line-height: 1.2;
letter-spacing: 0;
}
p {
margin: 0 0 24px;
color: var(--muted);
line-height: 1.7;
font-size: 15px;
}
a {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0 18px;
border-radius: 999px;
background: var(--blue);
color: #fff;
text-decoration: none;
font-weight: 700;
}
code {
display: block;
margin-top: 18px;
overflow-wrap: anywhere;
color: var(--muted);
font-size: 12px;
}
</style>
</head>
<body>
<main>
<h1>正在跳转到 Multica Xiaomi</h1>
<p>这是小米内网入口,目标页面托管在 GitHub Pages。</p>
<a href="https://park0er.github.io/multi-casual-android/xiaomi/">立即打开</a>
<code>https://park0er.github.io/multi-casual-android/xiaomi/</code>
</main>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+662
View File
@@ -0,0 +1,662 @@
import logoImg from "./assets/logo.png";
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { motion, AnimatePresence, useMotionValue, useTransform, useReducedMotion } from "motion/react";
import inboxImg from "./assets/screenshots/01-inbox.jpg";
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 = {
packageName: "ai.multica.app.xiaomi",
sha256: "13e004472d33e66aa40aa7ebd58f75e22cfde8cce3a153729481f1be4d2d1414",
gitlabUrl: "https://git.n.xiaomi.com/zhaoxisheng/multica-xiaomi-android",
releasesUrl: "https://github.com/park0er/multica-xiaomi-apk/releases/download/v0.1.5-xiaomi/Multica-Xiaomi-v0.1.5.apk",
iosUrl: "https://github.com/park0er/multi-casual-ios-app",
multicaUrl: "https://multica.ai",
apiUrl: "http://staging-multica.ad.xiaomi.srv",
};
type Locale = "en" | "zh";
const copy: Record<Locale, {
nav: { brand: string; download: string; trust: string; language: string };
hero: { eyebrow: string; title: string; subtitle: string; download: string; gitlab: string; multica: string; ios: string };
// relationship section intentionally removed for the internal build
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; note: string };
trust: { kicker: string; items: string[] };
footer: { tagline: string; multica: string; gitlab: string; ios: string; download: string };
}> = {
en: {
nav: { brand: "Multica Xiaomi", download: "Download", trust: "Scope", language: "中文" },
hero: {
eyebrow: "Internal Xiaomi build · Company-only distribution",
title: "Multica Xiaomi for the Xiaomi workspace.",
subtitle: "An Android app independently built from scratch by Zhao Xisheng in Xiaomi's Commercial Platform Department. Multica has not provided an official Android app to date; this internal build gives Xiaomi users a native path today.",
download: "Download APK",
gitlab: "GitLab",
multica: "Visit Multica",
ios: "iOS App",
},
journey: {
kicker: "Interactive walkthrough",
title: "Try the flow.",
subtitle: "Click each step or scroll to see the app in action.",
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: "Browse issues", subtitle: "See all tasks in a clean list view, ready for triage. Filter, search, and tap into any issue." },
{ 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: "Scroll for more", subtitle: "Keep scrolling to see subtasks, subscribers, and the comment thread — all the structured context stays on one page." },
{ 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." },
],
},
workspace: {
kicker: "Workspace assets",
title: "Projects, agents, and everything in between.",
subtitle: "Switch between workspace views to see the full picture.",
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" },
],
},
video: { kicker: "Product demo", title: "See it in 30 seconds.", body: "A fast walkthrough of inbox, issues, context, comments, mentions, and workspace views." },
download: { kicker: "Internal build", title: "Download the Xiaomi internal APK from GitHub Pages.", package: "Package", api: "Service API", sha: "APK SHA256", latest: "Download APK", source: "View source", note: "Internal distribution only — not for external release." },
trust: {
kicker: "Internal build scope",
items: [
"Designed and implemented from scratch by Zhao Xisheng of Xiaomi's Commercial Platform Department, with no upstream Android app to inherit from.",
"Multica has not provided an official Android app to date; this project is an independent Xiaomi internal Android client for Multica workflows.",
"Company-internal distribution only. APK download is served from a separate GitHub release artifact repository after EdgeOne proved unreliable.",
"Connects to the Xiaomi Multica service endpoint, with analytics routed through OneTrack and disabled for Umeng / PostHog.",
"Source is kept in the private company GitLab project; the signed APK is distributed from GitHub Pages.",
],
},
footer: { tagline: "Built from scratch by Zhao Xisheng for the Xiaomi workspace. APK served via GitHub Pages.", multica: "Multica website", gitlab: "GitLab", ios: "iOS App", download: "Download APK" },
},
zh: {
nav: { brand: "Multica Xiaomi", download: "下载", trust: "分发说明", language: "English" },
hero: {
eyebrow: "小米内部构建 · 仅限公司内部分发",
title: "面向小米工作区的 Multica Xiaomi 内部版。",
subtitle: "由小米商业平台部赵锡盛完全从头自研的 Android App。Multica 官方到目前为止还没有提供 Android App;这套内部构建先为小米用户补上原生移动端入口。",
download: "下载 APK",
gitlab: "GitLab",
multica: "访问 Multica",
ios: "iOS App",
},
journey: {
kicker: "交互式演示",
title: "试试这个流程。",
subtitle: "点击每个步骤,或滚动页面查看 app 实际操作。",
cta: "开始演示",
steps: [
{ title: "Inbox 通知", subtitle: "新 issues、评论和 agent 进展会实时出现在收件箱,不错过任何动态。" },
{ title: "浏览 Issues", subtitle: "在列表视图中查看所有任务,快速筛选和进入任意 issue。" },
{ title: "切换看板视图", subtitle: "点击视图切换按钮,看板视图让你一眼看到任务状态流。" },
{ title: "打开 Issue 详情", subtitle: "点击 issue 卡片查看完整详情:标题、状态、标签、负责人和正文。" },
{ title: "探索上下文", subtitle: "Context 引导点依次放大标题与状态、正文内容、子任务与评论,然后缩回全貌。" },
{ title: "滚动查看更多", subtitle: "继续下滑查看子任务、订阅者和评论线程 —— 结构化上下文都在同一页。" },
{ title: "草拟评论", subtitle: "点击评论框,键盘出现,评论逐字输入。" },
{ title: "@ 提及任何人", subtitle: "输入 @ 弹出选择器,可选人、agent 或小队。" },
{ title: "Workspace 总览", subtitle: "通过 workspace chips 切换查看 Projects、Agents、Runtimes、Skills 和 Settings。" },
],
},
workspace: {
kicker: "Workspace 资产",
title: "项目、agents,以及它们之间的一切。",
subtitle: "切换 workspace 视图,掌握全貌。",
items: [
{ title: "Projects", subtitle: "浏览项目和进度追踪" },
{ title: "项目详情", subtitle: "任务统计、状态字段和资源入口" },
{ title: "Agents", subtitle: "查看 agent 状态和最近活动" },
{ title: "Runtimes", subtitle: "检查运行时可用性和版本" },
{ title: "Skills", subtitle: "浏览 workspace agent 技能库" },
{ title: "技能详情", subtitle: "查看技能结构和文档" },
{ title: "Settings", subtitle: "账号、工作区和偏好控制" },
],
},
video: { kicker: "产品演示", title: "30 秒看完核心流程。", body: "快速演示 inbox 通知、issues 看板、上下文探索、评论回复、@ 提及和 workspace 视图切换。" },
download: { kicker: "内部构建", title: "从 GitHub Pages 下载小米内部 APK。", package: "Package", api: "Service API", sha: "APK SHA256", latest: "下载 APK", source: "查看源码", note: "仅供公司内部分发,不对外发布。" },
trust: {
kicker: "内部构建说明",
items: [
"由小米商业平台部赵锡盛完全从头设计和实现,不是基于 Multica 官方 Android 客户端改造。",
"Multica 官方到目前为止还没有提供 Android App;本项目是面向小米内部 Multica 工作流的独立 Android 客户端。",
"仅供公司内部分发;由于腾讯云 EdgeOne 访问不稳定,APK 下载改由单独的 GitHub Release 产物仓库托管提供。",
"连接小米 Multica 服务端点,统计走 OneTrackUmeng / PostHog 已关闭。",
"源码保存在私有公司 GitLab 项目中;签名 APK 从 GitHub Pages 分发。",
],
},
footer: { tagline: "商业平台部赵锡盛从头自研,APK 由 GitHub Pages 分发。", multica: "Multica 官网", gitlab: "GitLab", ios: "iOS App", download: "下载 APK" },
},
};
const JOURNEY_STEPS = 9;
const TYPING_TEXT = "Looks good. I'll ask the agent to check the failing case and post a follow-up.";
const MENTION_ITEMS = [
{ name: "codex", color: "#1647ff" },
{ name: "Opus", color: "#7c3aed" },
{ name: "RollieOC", color: "#059669" },
];
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>
);
}
/* ── Typewriter effect ── */
function TypewriterEffect({ active, text, reduced }: { active: boolean; text: string; reduced: boolean }) {
const charCount = useMotionValue(0);
const chars = useMemo(() => text.split(""), [text]);
const charRefs = useRef<(HTMLSpanElement | null)[]>([]);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Imperatively update char opacity from motion value (no hooks in loops)
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]);
return (
<motion.div
className="typing-line"
animate={active ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 0.3 }}
>
{chars.map((char, i) => (
<span
key={i}
ref={(el) => { charRefs.current[i] = el; }}
style={{ opacity: 0 }}
>
{char === " " ? "\u00A0" : char}
</span>
))}
<motion.span
className="typing-cursor"
animate={active ? { opacity: [1, 0.18] } : { opacity: 0 }}
transition={active ? { duration: 0.64, repeat: Infinity, repeatType: "reverse" } : {}}
/>
</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() {
const reduced = useReducedMotion() ?? false;
const [lang, setLang] = useState<Locale>("en");
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 (
<main lang={lang === "zh" ? "zh-CN" : "en"}>
{/* Hero */}
<section className="hero">
<div className="hero-bg" />
<motion.nav className="topbar" {...fade(reduced, 0)}>
<div className="brand">
<img src={logoImg} alt="Multica Xiaomi" className="brand-mark" />
<span>{t.nav.brand}</span>
</div>
<div className="nav-links">
<a href="#download">{t.nav.download}</a>
<a href="#trust">{t.nav.trust}</a>
<button className="language-toggle" onClick={() => setLang((l) => (l === "en" ? "zh" : "en"))}>
{t.nav.language}
</button>
</div>
</motion.nav>
<div className="hero-grid">
<div>
<motion.p className="eyebrow" {...fade(reduced, 0.04)}>{t.hero.eyebrow}</motion.p>
<motion.h1 {...fade(reduced, 0.14)}>{t.hero.title}</motion.h1>
<motion.p className="hero-subtitle" {...fade(reduced, 0.24)}>{t.hero.subtitle}</motion.p>
<motion.div className="cta-row" {...fade(reduced, 0.34)}>
<motion.a href="#download" className="pill pill-android" whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}><span className="android-icon"></span>{t.hero.download}</motion.a>
<motion.a className="button secondary" href={release.gitlabUrl} 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="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/></svg>{t.hero.gitlab}</motion.a>
<motion.a className="button ghost" href={release.multicaUrl} whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}>{t.hero.multica}</motion.a>
<motion.a href={release.iosUrl} className="pill pill-ios" whileHover={{ y: -2 }} whileTap={{ scale: 0.98 }}><span className="apple-icon"></span>{t.hero.ios}</motion.a>
</motion.div>
</div>
<PhoneMockup reduced={reduced} />
</div>
</section>
{/* (Relationship section removed for the internal build.) */}
{/* Interactive Product Journey */}
<section ref={journeyRef} className="journey section-pad">
<motion.div className="journey-header" {...reveal(reduced)}>
<div className="section-kicker">{t.journey.kicker}</div>
<h2>{t.journey.title}</h2>
<p>{t.journey.subtitle}</p>
</motion.div>
<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 className="workspace-phone-wrap">
<AnimatePresence mode="wait">
<motion.div
key={activeWs}
initial={reduced ? {} : { opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
exit={reduced ? {} : { opacity: 0, scale: 0.96 }}
transition={{ duration: 0.35 }}
>
<PhoneDemo src={WORKSPACE_SCREENSHOTS[activeWs]} reduced={reduced} />
</motion.div>
</AnimatePresence>
</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>
{/* Video Demo */}
<motion.section className="video-demo section-pad" {...reveal(reduced)}>
<div className="section-kicker">{t.video.kicker}</div>
<div className="video-copy">
<h2>{t.video.title}</h2>
<p>{t.video.body}</p>
</div>
<video className="promo-video" src={promoVideo} controls playsInline preload="metadata" />
</motion.section>
{/* Download */}
<section id="download" className="download section-pad">
<motion.div className="release-card" {...reveal(reduced)}>
<div className="section-kicker">{t.download.kicker}</div>
<h2>{t.download.title}</h2>
<div className="release-grid">
<div>
<span>{t.download.package}</span>
<code>{release.packageName}</code>
</div>
<div>
<span>{t.download.api}</span>
<code>{release.apiUrl}</code>
</div>
<div className="hash-cell">
<span>{t.download.sha}</span>
<code>{release.sha256}</code>
</div>
</div>
<pre><code>{`adb install -r Multica-Xiaomi-v0.1.5.apk`}</code></pre>
<div className="cta-row compact">
<a href={release.releasesUrl} className="pill pill-android"><span className="android-icon"></span>{t.download.latest}</a>
<a className="button secondary" href={release.gitlabUrl}>{t.download.source}</a>
</div>
<p className="star-hint">
{lang === "en"
? <>APK downloads are served by a separate GitHub release artifact repository; source lives on the company <a href={release.gitlabUrl} target="_blank" rel="noopener">GitLab</a>.</>
: <>APK GitHub Release <a href={release.gitlabUrl} target="_blank" rel="noopener">GitLab</a></>
}
</p>
</motion.div>
</section>
{/* Trust */}
<section id="trust" className="trust section-pad">
<div className="section-kicker">{t.trust.kicker}</div>
<div className="trust-grid">
{t.trust.items.map((item) => (
<motion.div key={item} className="trust-item" {...reveal(reduced)}>
<span className="check"></span>
<p>{item}</p>
</motion.div>
))}
</div>
</section>
{/* Footer */}
<footer className="footer section-pad">
<div>
<strong>Multica Xiaomi</strong>
<p>{t.footer.tagline}</p>
</div>
<div className="footer-links">
<a href={release.multicaUrl}>{t.footer.multica}</a>
<a href={release.gitlabUrl}>{t.footer.gitlab}</a>
<a href={release.releasesUrl} className="pill pill-android"><span className="android-icon"></span>{t.footer.download}</a>
<a href={release.iosUrl} className="pill pill-ios"><span className="apple-icon"></span>{t.footer.ios}</a>
</div>
</footer>
</main>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+971
View File
@@ -0,0 +1,971 @@
:root {
--blue: #1647ff;
--blue-2: #0f33c8;
--yellow: #ffe45c;
--ink: #101218;
--muted: #687080;
--paper: #f6f8fc;
--card: #ffffff;
--line: rgba(16, 18, 24, 0.1);
--shadow: 0 24px 70px rgba(16, 18, 24, 0.14);
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
color: var(--ink);
background: var(--paper);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
min-width: 320px;
overflow-x: hidden;
background:
radial-gradient(circle at 82% 5%, rgba(255, 228, 92, 0.4), transparent 31%),
radial-gradient(circle at 10% 30%, rgba(22, 71, 255, 0.16), transparent 30%),
linear-gradient(180deg, #fbfcff 0%, #e8eeff 55%, #d4dff9 85%, #c4d4f5 100%);
}
body, button, input { font: inherit; }
button { cursor: pointer; }
a { color: inherit; text-decoration: none; }
.section-pad { padding: 80px clamp(22px, 5vw, 76px); }
.hero {
min-height: 100vh;
position: relative;
overflow: hidden;
padding-top: 26px;
padding-left: clamp(16px, 5vw, 76px);
padding-right: clamp(16px, 5vw, 76px);
}
.hero-bg {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(16, 18, 24, 0.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(16, 18, 24, 0.045) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: linear-gradient(to bottom, black 0%, transparent 88%);
pointer-events: none;
}
.topbar {
width: min(1160px, 100%);
margin: 0 auto 70px;
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(255, 255, 255, 0.72);
backdrop-filter: blur(18px);
box-shadow: 0 12px 36px rgba(16, 18, 24, 0.06);
}
.brand, .nav-links { display: flex; align-items: center; gap: 10px; }
.brand { font-weight: 850; letter-spacing: -0.02em; }
.brand-mark {
width: 34px;
height: 34px;
border-radius: 10px;
object-fit: cover;
display: block;
}
.nav-links a {
color: var(--muted);
font-size: 14px;
font-weight: 700;
padding: 8px 10px;
}
.nav-links a:hover { color: var(--ink); }
.language-toggle {
color: var(--blue);
font-size: 14px;
font-weight: 850;
padding: 8px 12px;
border: 1px solid rgba(22, 71, 255, 0.18);
border-radius: 999px;
background: rgba(22, 71, 255, 0.07);
}
.language-toggle:hover { background: rgba(22, 71, 255, 0.12); }
.hero-grid {
width: min(1160px, 100%);
margin: 0 auto;
position: relative;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 1.06fr) minmax(320px, 0.72fr);
gap: clamp(28px, 7vw, 96px);
align-items: center;
}
.eyebrow, .section-kicker {
margin: 0 0 16px;
color: var(--blue);
font-size: 12px;
font-weight: 900;
letter-spacing: 0.15em;
text-transform: uppercase;
}
h1, h2, h3, p { margin-top: 0; }
h1 {
max-width: 780px;
margin-bottom: 24px;
font-size: clamp(54px, 8.2vw, 112px);
line-height: 0.9;
letter-spacing: -0.075em;
}
h2 {
font-size: clamp(36px, 5vw, 72px);
line-height: 0.94;
letter-spacing: -0.065em;
margin-bottom: 22px;
}
main[lang="zh-CN"] h1 {
max-width: 700px;
font-size: clamp(42px, 6.8vw, 86px);
line-height: 0.98;
letter-spacing: -0.07em;
}
main[lang="zh-CN"] h2 {
font-size: clamp(34px, 4.6vw, 62px);
line-height: 1;
}
h3 {
font-size: 24px;
line-height: 1.08;
letter-spacing: -0.035em;
}
.hero-subtitle, .relationship p, .demo p, .journey-desc {
max-width: 650px;
color: #343b4a;
font-size: clamp(18px, 2vw, 23px);
line-height: 1.48;
}
.cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
.cta-row.compact { margin-top: 22px; }
.button {
font-size: 15px;
display: inline-flex;
min-height: 48px;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 0 20px;
font-weight: 850;
border: 1px solid transparent;
transition: color 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}
.button.primary { color: white; background: var(--blue); box-shadow: 0 14px 34px rgba(22, 71, 255, 0.32); }
.button.secondary { background: white; border-color: var(--line); }
.button.ghost { color: var(--muted); background: rgba(255, 255, 255, 0.54); border-color: var(--line); }
/* Phone mockup */
.phone-wrap {
width: min(390px, 82vw);
margin: 0 auto;
position: relative;
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 {
position: relative;
padding: 14px;
border-radius: 54px;
background: #11141d;
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 {
position: absolute;
top: 18px;
left: 50%;
width: 72px;
height: 8px;
transform: translateX(-50%);
border-radius: 999px;
background: rgba(255,255,255,0.18);
z-index: 2;
}
.phone-screen {
position: relative;
overflow: hidden;
border-radius: 40px;
background: white;
aspect-ratio: 9 / 19.7;
}
.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 {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: top center;
}
.tap-ring {
position: absolute;
right: -8px;
bottom: 158px;
width: 76px;
height: 76px;
border-radius: 999px;
border: 2px solid var(--blue);
background: rgba(255, 228, 92, 0.7);
box-shadow: 0 0 0 16px rgba(255, 228, 92, 0.22);
}
/* Shared content sections */
.relationship, .features, .demo, .video-demo, .download, .trust, .footer {
width: min(1160px, calc(100% - 44px));
margin: 0 auto;
padding-left: 0;
padding-right: 0;
}
.two-col, .demo-grid {
display: grid;
grid-template-columns: minmax(0, 0.85fr) minmax(320px, 1fr);
gap: clamp(24px, 5vw, 76px);
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 {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 18px;
}
.feature-card, .trust-item, .release-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);
}
.feature-card { padding: 28px; min-height: 250px; }
.feature-card span { color: var(--blue); font-weight: 950; font-size: 13px; letter-spacing: 0.12em; }
.feature-card p, .trust-item p { color: var(--muted); line-height: 1.65; }
.demo { overflow: hidden; }
.screenshot-stack { min-height: 560px; position: relative; overflow: hidden; border-radius: 42px; }
.shot {
position: absolute;
width: min(310px, 52vw);
height: 620px;
border-radius: 34px;
border: 10px solid #11141d;
box-shadow: var(--shadow);
background: white;
object-fit: cover;
object-position: top center;
}
.shot.back { right: 210px; top: 40px; transform: rotate(-7deg); opacity: 0.78; }
.shot.front { right: 12px; top: 0; transform: rotate(5deg); }
.video-demo {
padding-top: 20px;
}
.video-copy {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(320px, 1fr);
gap: clamp(24px, 5vw, 76px);
align-items: end;
margin-bottom: 28px;
}
.video-copy p {
color: #343b4a;
font-size: clamp(17px, 1.7vw, 21px);
line-height: 1.55;
}
.promo-video, .video-placeholder {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
border: 1px solid var(--line);
border-radius: 34px;
background: #0e1320;
box-shadow: var(--shadow);
}
.promo-video { object-fit: cover; z-index: 1; position: relative; }
.video-placeholder {
display: grid;
place-items: center;
color: white;
background:
radial-gradient(circle at 70% 24%, rgba(255, 228, 92, 0.22), transparent 34%),
linear-gradient(135deg, #0e1320, #172453);
}
.video-placeholder span {
width: 86px;
height: 86px;
display: grid;
place-items: center;
margin-bottom: 18px;
border-radius: 999px;
color: var(--blue);
background: var(--yellow);
font-size: 44px;
font-weight: 950;
box-shadow: 0 18px 42px rgba(255, 228, 92, 0.24);
}
.video-placeholder strong {
display: block;
color: white;
font-size: clamp(22px, 3vw, 42px);
letter-spacing: -0.04em;
}
.download { padding-top: 40px; }
.release-card { padding: clamp(28px, 5vw, 54px); }
.release-grid { margin: 30px 0 22px; }
.release-grid div {
min-width: 0;
padding: 18px;
border-radius: 20px;
background: #f7f9ff;
border: 1px solid rgba(22, 71, 255, 0.13);
}
.release-grid span { display: block; color: var(--muted); font-size: 12px; font-weight: 900; text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 8px; }
.release-grid code, pre code { font-family: "SFMono-Regular", ui-monospace, Menlo, Consolas, "PingFang SC", monospace; }
.release-grid code { display: block; overflow-wrap: anywhere; color: var(--ink); font-size: 14px; }
.hash-cell { grid-column: span 3; }
pre {
overflow: auto;
margin: 0;
padding: 18px 20px;
border-radius: 20px;
color: #eef4ff;
background: #0e1320;
}
.trust-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.trust-item { display: flex; gap: 14px; padding: 22px; align-items: flex-start; }
.check {
width: 30px;
height: 30px;
display: grid;
place-items: center;
flex: none;
color: white;
border-radius: 999px;
background: var(--blue);
font-weight: 900;
}
.footer {
display: flex;
justify-content: space-between;
gap: 24px;
align-items: flex-start;
color: var(--muted);
border-top: 1px solid var(--line);
}
.footer strong { color: var(--ink); }
.footer-links { display: flex; gap: 18px; flex-wrap: wrap; font-weight: 800; color: var(--ink); }
/* Responsive */
@media (max-width: 900px) {
.topbar { border-radius: 24px; align-items: flex-start; flex-direction: column; }
.nav-links { flex-wrap: wrap; }
.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; }
.screenshot-stack { min-height: 680px; }
.shot { width: min(300px, 74vw); }
.shot.back { right: auto; left: 4%; }
.shot.front { right: 4%; top: 160px; }
.footer { flex-direction: column; }
.workspace-chips { justify-content: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !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;
}
/* ── Footer pills ── */
.pill {
display: inline-flex; align-items: center; gap: 7px;
padding: 10px 22px; border-radius: 999px;
font-weight: 850; font-size: 15px; letter-spacing: -0.01em;
text-decoration: none; transition: all 0.22s;
}
.pill-android {
background: var(--blue); color: #fff;
box-shadow: 0 12px 30px rgba(22,71,255,0.30);
}
.pill-android:hover { background: var(--blue-2); transform: translateY(-1px); }
.pill-android .android-icon {
width: 15px; height: 15px; display: inline-block; background: #fff;
-webkit-mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYgMThjMCAuNTUuNDUgMSAxIDFoMXYzLjVjMCAuODMuNjcgMS41IDEuNSAxLjVzMS41LS42NyAxLjUtMS41VjE5aDJ2My41YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNVYxOWgxYy41NSAwIDEtLjQ1IDEtMVY4SDZ2MTB6TTMuNSA4QzIuNjcgOCAyIDguNjcgMiA5LjV2N2MwIC44My42NyAxLjUgMS41IDEuNVM1IDE3LjMzIDUgMTYuNXYtN0M1IDguNjcgNC4zMyA4IDMuNSA4em0xNyAwYy0uODMgMC0xLjUuNjctMS41IDEuNXY3YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNXYtN2MwLS44My0uNjctMS41LTEuNS0xLjV6bS00Ljk3LTUuODRsMS4zLTEuM2MuMi0uMi4yLS41MSAwLS43MS0uMi0uMi0uNTEtLjItLjcxIDBsLTEuNDggMS40OEE1Ljg0IDUuODQgMCAwMDEyIDFjLS45NiAwLTEuODYuMjMtMi42Ni42M0w3Ljg1LjE1Yy0uMi0uMi0uNTEtLjItLjcxIDAtLjIuMi0uMi41MSAwIC43MWwxLjMxIDEuMzFBNS45ODMgNS45ODMgMCAwMDYgN2gxMmMwLTIuMTItMS4xLTMuOTgtMi40Ny00Ljg0ek0xMCA1SDlWNGgxdjF6bTUgMGgtMVY0aDF2MXoiLz48L3N2Zz4=) center/contain no-repeat;
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYgMThjMCAuNTUuNDUgMSAxIDFoMXYzLjVjMCAuODMuNjcgMS41IDEuNSAxLjVzMS41LS42NyAxLjUtMS41VjE5aDJ2My41YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNVYxOWgxYy41NSAwIDEtLjQ1IDEtMVY4SDZ2MTB6TTMuNSA4QzIuNjcgOCAyIDguNjcgMiA5LjV2N2MwIC44My42NyAxLjUgMS41IDEuNVM1IDE3LjMzIDUgMTYuNXYtN0M1IDguNjcgNC4zMyA4IDMuNSA4em0xNyAwYy0uODMgMC0xLjUuNjctMS41IDEuNXY3YzAgLjgzLjY3IDEuNSAxLjUgMS41czEuNS0uNjcgMS41LTEuNXYtN2MwLS44My0uNjctMS41LTEuNS0xLjV6bS00Ljk3LTUuODRsMS4zLTEuM2MuMi0uMi4yLS41MSAwLS43MS0uMi0uMi0uNTEtLjItLjcxIDBsLTEuNDggMS40OEE1Ljg0IDUuODQgMCAwMDEyIDFjLS45NiAwLTEuODYuMjMtMi42Ni42M0w3Ljg1LjE1Yy0uMi0uMi0uNTEtLjItLjcxIDAtLjIuMi0uMi41MSAwIC43MWwxLjMxIDEuMzFBNS45ODMgNS45ODMgMCAwMDYgN2gxMmMwLTIuMTItMS4xLTMuOTgtMi40Ny00Ljg0ek0xMCA1SDlWNGgxdjF6bTUgMGgtMVY0aDF2MXoiLz48L3N2Zz4=) center/contain no-repeat;
}
.pill-ios {
position: relative; overflow: hidden;
background: rgba(255,255,255,0.22);
backdrop-filter: blur(20px) saturate(1.8);
-webkit-backdrop-filter: blur(20px) saturate(1.8);
color: #1a1a2e; font-weight: 850;
border: 1px solid rgba(255,255,255,0.55);
box-shadow: 0 4px 20px rgba(0,0,0,0.08),
inset 0 1px 0 rgba(255,255,255,0.9),
inset 0 -1px 0 rgba(0,0,0,0.04);
}
.pill-ios:hover {
background: rgba(255,255,255,0.38);
transform: translateY(-1px);
box-shadow: 0 8px 28px rgba(0,0,0,0.12),
inset 0 1px 0 rgba(255,255,255,1),
inset 0 -1px 0 rgba(0,0,0,0.04);
}
.pill-ios::before {
content: "";
position: absolute; top: 0; left: -40%; width: 180%; height: 50%;
background: linear-gradient(180deg, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0) 100%);
border-radius: 999px; pointer-events: none;
}
.pill-ios::after {
content: "";
position: absolute; bottom: -2px; left: 10%; width: 80%; height: 6px;
background: linear-gradient(90deg, #ff6b9d, #c44dff, #6b9dff, #45d4a8);
filter: blur(5px); opacity: 0.4; border-radius: 999px;
pointer-events: none; z-index: -1;
}
.pill-ios .apple-icon {
width: 14px; height: 14px; display: inline-block; background: #1a1a2e;
-webkit-mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE4LjcxIDE5LjVjLS44MyAxLjI0LTEuNzEgMi40NS0zLjA1IDIuNDctMS4zNC4wMy0xLjc3LS43OS0zLjI5LS43OS0xLjUzIDAtMiAuNzctMy4yNy44Mi0xLjMxLjA1LTIuMy0xLjMyLTMuMTQtMi41M0M0LjI1IDE3IDIuOTQgMTIuNDUgNC43IDkuMzljLjg3LTEuNTIgMi40My0yLjQ4IDQuMTItMi41MSAxLjI4LS4wMiAyLjUuODcgMy4yOS44Ny43OCAwIDIuMjYtMS4wNyAzLjgxLS45MS42NS4wMyAyLjQ3LjI2IDMuNjQgMS45OC0uMDkuMDYtMi4xNyAxLjI4LTIuMTUgMy44MS4wMyAzLjAyIDIuNjUgNC4wMyAyLjY4IDQuMDQtLjAzLjA3LS40MiAxLjQ0LTEuMzggMi44M00xMyAzLjVjLjczLS44MyAxLjk0LTEuNDYgMi45NC0xLjUuMTMgMS4xNy0uMzQgMi4zNS0xLjA0IDMuMTktLjY5Ljg1LTEuODMgMS41MS0yLjk1IDEuNDItLjE1LTEuMTUuNDEtMi4zNSAxLjA1LTMuMTF6Ii8+PC9zdmc+) center/contain no-repeat;
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE4LjcxIDE5LjVjLS44MyAxLjI0LTEuNzEgMi40NS0zLjA1IDIuNDctMS4zNC4wMy0xLjc3LS43OS0zLjI5LS43OS0xLjUzIDAtMiAuNzctMy4yNy44Mi0xLjMxLjA1LTIuMy0xLjMyLTMuMTQtMi41M0M0LjI1IDE3IDIuOTQgMTIuNDUgNC43IDkuMzljLjg3LTEuNTIgMi40My0yLjQ4IDQuMTItMi41MSAxLjI4LS4wMiAyLjUuODcgMy4yOS44Ny43OCAwIDIuMjYtMS4wNyAzLjgxLS45MS42NS4wMyAyLjQ3LjI2IDMuNjQgMS45OC0uMDkuMDYtMi4xNyAxLjI4LTIuMTUgMy44MS4wMyAzLjAyIDIuNjUgNC4wMyAyLjY4IDQuMDQtLjAzLjA3LS40MiAxLjQ0LTEuMzggMi44M00xMyAzLjVjLjczLS44MyAxLjk0LTEuNDYgMi45NC0xLjUuMTMgMS4xNy0uMzQgMi4zNS0xLjA0IDMuMTktLjY5Ljg1LTEuODMgMS41MS0yLjk1IDEuNDItLjE1LTEuMTUuNDEtMi4zNSAxLjA1LTMuMTF6Ii8+PC9zdmc+) center/contain no-repeat;
}
@media (max-width: 600px) {
.pill { padding: 8px 16px; font-size: 14px; }
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": []
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(() => ({
plugins: [react()],
base: process.env.VITE_BASE || '/',
build: {
outDir: 'dist',
},
server: {
host: '127.0.0.1',
},
}));