Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0294e1f
feat(ecosystem): add MiniappSplashScreen component with glow effect
Gaubee Dec 30, 2025
f19a170
feat(services): add miniapp-runtime service for app lifecycle management
Gaubee Dec 30, 2025
5310bd8
feat(ecosystem): add three-page layout with Parallax wallpaper
Gaubee Dec 30, 2025
eda7153
feat(ecosystem): add MiniappWindow with FLIP animation support
Gaubee Dec 30, 2025
6170545
feat(ecosystem): add MiniappCapsule and update window layout
Gaubee Dec 30, 2025
63a38bd
feat(ecosystem): add MiniappStackView and MiniappStackCard components
Gaubee Dec 30, 2025
44e5643
feat(ecosystem): add EcosystemTabIndicator with bidirectional binding
Gaubee Dec 30, 2025
a13b6df
docs: add CSS Modules best practice to white book
Gaubee Dec 30, 2025
005ccf3
fix(e2e): update ecosystem tests for Swiper-based navigation
Gaubee Dec 30, 2025
c8f542d
fix(ecosystem): calculate Parallax wallpaper width based on offset
Gaubee Dec 30, 2025
6f9bad3
fix(ecosystem): discover-header scroll-driven background with color-mix
Gaubee Dec 30, 2025
be19fd1
fix(ecosystem): discover-header progressive enhancement for animation…
Gaubee Dec 30, 2025
17c865a
feat(ecosystem): Swiper 双向同步 + TabBar 指示器优化
Gaubee Dec 30, 2025
d560e3b
fix(miniapp): 修复启动屏和胶囊按钮问题
Gaubee Dec 30, 2025
74a9743
feat(ecosystem): EcosystemDesktop 组件封装
Gaubee Dec 31, 2025
3c1ce1c
chore(miniapps): checkpoint before icon-splash shared layout fix
Gaubee Jan 2, 2026
94f94cc
feat(miniapps): slide grid slot 驱动 miniapp-window
Gaubee Jan 2, 2026
03614bf
feat(miniapp-runtime): add MiniappFlow to runtime and settleFlow API
Gaubee Jan 3, 2026
c8f610d
Gaubee Jan 3, 2026
fcf75a3
feat(miniapp-runtime): centralize miniapp visual config
Gaubee Jan 3, 2026
99f27fa
refactor(ecosystem): remove legacy miniapp route
Gaubee Jan 3, 2026
e968bed
fix(ecosystem): force slide to targetDesktop when app running
Gaubee Jan 3, 2026
fd9f9e0
fix(ecosystem): capsule themes and avoid tab bar overlap
Gaubee Jan 4, 2026
ac4d83c
fix(miniapps): wire runtime bio permission and splash close
Gaubee Jan 4, 2026
6171c75
fix(crypto): stabilize bioforest address derivation
Gaubee Jan 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
],
"settings": {
"react": {
"version": "detect"
"version": "19"
}
}
}
26 changes: 26 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ import type { DecoratorFunction } from 'storybook/internal/types'
import { useEffect, useMemo } from 'react'
import { I18nextProvider } from 'react-i18next'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { LayoutGroup, MotionConfig } from 'motion/react'
import i18n, { languages, getLanguageDirection, type LanguageCode } from '../src/i18n'
import { currencies, preferencesActions, preferencesStore, type CurrencyCode } from '../src/stores/preferences'
import '../src/styles/globals.css'

const MOTION_DEBUG_SPEED = 0.2
const MOTION_DEBUG_TRANSITION = {
type: 'spring',
// Slow motion: 0.2x speed => ~5x longer.
// Scale stiffness by speed^2 and damping by speed to keep damping ratio similar.
stiffness: 220 * MOTION_DEBUG_SPEED * MOTION_DEBUG_SPEED,
damping: 28 * MOTION_DEBUG_SPEED,
mass: 0.85,
} as const

const mobileViewports = {
iPhoneSE: {
name: 'iPhone SE',
Expand Down Expand Up @@ -124,6 +135,15 @@ const preview: Preview = {
},
},
decorators: [
// Global slow-motion for animation debugging
((Story) => (
<MotionConfig transition={MOTION_DEBUG_TRANSITION}>
<LayoutGroup id="miniapp-shared-layout">
<Story />
</LayoutGroup>
</MotionConfig>
)) as DecoratorFunction<ReactRenderer>,

// i18n + Theme + Direction + QueryClient decorator
((Story, context) => {
const locale = (context.globals['locale'] || 'zh-CN') as LanguageCode
Expand Down Expand Up @@ -179,6 +199,12 @@ const preview: Preview = {

// Container size decorator with theme wrapper
((Story, context) => {
// Fullscreen stories should not be wrapped in a resizable container.
// Many app-level screens (e.g. MiniappWindow) rely on fullscreen canvas layout.
if (context.parameters?.layout === 'fullscreen') {
return <Story />
}

const containerKey = context.globals['containerSize'] || 'standard'
const containerWidth = containerSizes[containerKey as keyof typeof containerSizes]?.width || 360
const theme = context.globals['theme'] || 'light'
Expand Down
4 changes: 4 additions & 0 deletions docs/white-book/00-必读/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@
- 紧凑头部效果使用 `animation-range: 0 80px` 限制动画范围
- ⚠️ scroll-driven animations 是渐进增强:初始状态必须是可用的(如 opacity-0),不支持时保持初始状态
- E2E 截图变更后运行 `pnpm e2e:audit` 检查残留截图,详见白皮书 08-测试篇/03-Playwright配置/e2e-best-practices
- 组件专属样式使用 CSS Modules:`component-name.module.css` + `import styles from './xxx.module.css'`
- CSS Modules 适用场景:@keyframes 动画、伪元素(::before/::after)、复杂选择器(:focus-within)、scroll-driven animations
- CSS Modules 与 Tailwind 混用:`className={cn(styles.header, 'sticky top-0 z-10 px-5')}`
- 优先级:CSS Modules > globals.css,组件样式应内聚到组件目录
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 74 additions & 10 deletions e2e/ecosystem-miniapp.mock.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { test, expect } from '@playwright/test'
import { test, expect, type Page } from '@playwright/test'

/**
* Bio 小程序生态 E2E 截图测试
*
* 测试用户故事并生成截图验证 UI 正确性
*/

/**
* 滑动到"我的"页面 (Swiper 布局:从右向左滑动一次)
* 发现(0) → 我的(1) → 堆栈(2)
*/
async function swipeToMyAppsPage(page: Page) {
const viewport = page.viewportSize()!
// 从中右向中左滑动,距离适中,避免滑过头
await page.mouse.move(viewport.width * 0.7, viewport.height / 2)
await page.mouse.down()
await page.mouse.move(viewport.width * 0.3, viewport.height / 2, { steps: 20 })
await page.mouse.up()
await page.waitForTimeout(500)
}

const TEST_WALLET_DATA = {
wallets: [
{
Expand Down Expand Up @@ -112,9 +126,8 @@ test.describe('生态 Tab 截图测试', () => {
await ecosystemTab.click()
await page.waitForTimeout(500)

// 点击"我的" Tab
const myTab = page.locator('button:has-text("我的")')
await myTab.click()
// 滑动到"我的"页 (从右向左滑)
await swipeToMyAppsPage(page)
await page.waitForTimeout(500)

await expect(page).toHaveScreenshot('02b-ecosystem-my-tab.png')
Expand All @@ -138,9 +151,8 @@ test.describe('生态 Tab 截图测试', () => {
await ecosystemTab.click()
await page.waitForTimeout(500)

// 点击"我的" Tab
const myTab = page.locator('button:has-text("我的")')
await myTab.click()
// 滑动到"我的"页
await swipeToMyAppsPage(page)
await page.waitForTimeout(500)

// 右键点击第一个应用图标触发 Context Menu
Expand All @@ -166,7 +178,7 @@ test.describe('生态 Tab 截图测试', () => {
// 进入生态 - 我的
await page.getByTestId('tab-ecosystem').click()
await page.waitForTimeout(300)
await page.locator('button:has-text("我的")').click()
await swipeToMyAppsPage(page)
await page.waitForTimeout(300)

// 右键菜单 -> 打开
Expand All @@ -193,7 +205,7 @@ test.describe('生态 Tab 截图测试', () => {
// 进入生态 - 我的
await page.getByTestId('tab-ecosystem').click()
await page.waitForTimeout(300)
await page.locator('button:has-text("我的")').click()
await swipeToMyAppsPage(page)
await page.waitForTimeout(300)

// 右键菜单 -> 详情
Expand Down Expand Up @@ -221,7 +233,7 @@ test.describe('生态 Tab 截图测试', () => {
// 进入生态 - 我的
await page.getByTestId('tab-ecosystem').click()
await page.waitForTimeout(300)
await page.locator('button:has-text("我的")').click()
await swipeToMyAppsPage(page)
await page.waitForTimeout(300)

// 右键菜单 -> 移除
Expand Down Expand Up @@ -515,6 +527,58 @@ test.describe('权限请求截图测试', () => {
})
})

// ============================================
// 小程序权限集成测试(runtime + bridge + sheet)
// ============================================

test.describe('小程序权限集成测试', () => {
test.beforeEach(async ({ page }) => {
await injectTestData(page)
})

test('Teleport 启动传送门应弹出权限请求', async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem(
'ecosystem_my_apps',
JSON.stringify([{ appId: 'xin.dweb.teleport', installedAt: Date.now() - 3600000, lastUsedAt: Date.now() - 1800000 }])
)
})

await page.goto('/#/')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(500)

await page.getByTestId('tab-ecosystem').click()
await page.waitForTimeout(300)

await swipeToMyAppsPage(page)
await page.waitForTimeout(300)

// 右键菜单 -> 打开
await page.locator('[data-testid="ios-app-icon-xin.dweb.teleport"]').click({ button: 'right' })
await page.waitForTimeout(200)
await page.locator('button:has-text("打开")').click()

// 等待 iframe 加载并点击“启动传送门”
const teleportFrame = page.frameLocator('iframe[data-app-id="xin.dweb.teleport"]')
const launchButton = teleportFrame.getByRole('button', { name: '启动传送门' })
await launchButton.waitFor({ state: 'visible', timeout: 15000 })

await launchButton.click()

// 点击后应进入连接中状态
await expect(teleportFrame.getByRole('button', { name: '连接中...' })).toBeVisible({ timeout: 3000 })

// 应出现权限请求 Sheet
await expect(page.getByText('请求以下权限')).toBeVisible({ timeout: 8000 })
await expect(page.getByText('查看账户')).toBeVisible({ timeout: 8000 })

// 允许后应进入钱包选择器
await page.locator('button:has-text("允许")').click()
await expect(page.getByText('选择钱包')).toBeVisible({ timeout: 8000 })
})
})

// ============================================
// 空状态测试
// ============================================
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
"preview": "vite preview",
"test": "turbo run test:run --",
"test:run": "vitest run --project=unit",
"test:storybook": "vitest run --project=storybook",
"test:storybook": "bun scripts/storybook-clean.ts && vitest run --project=storybook",
"test:all": "vitest run",
"test:coverage": "vitest run --project=unit --coverage",
"test:ui": "vitest run --ui",
"storybook": "storybook dev -p 6006",
"storybook": "bun scripts/storybook-clean.ts && storybook dev -p 6006",
"build-storybook": "storybook build",
"typecheck": "turbo run typecheck:run --",
"typecheck:run": "tsc --build --noEmit",
Expand Down Expand Up @@ -105,6 +105,7 @@
"idb": "^8.0.3",
"jsqr": "^1.4.0",
"lodash": "^4.17.21",
"motion": "^12.23.26",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
32 changes: 29 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions scripts/storybook-clean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bun

import { rm } from 'node:fs/promises'
import { join } from 'node:path'

const ROOT = join(import.meta.dirname, '..')

const PATHS_TO_CLEAN = [
'node_modules/.cache/storybook',
]

async function clean() {
for (const path of PATHS_TO_CLEAN) {
await rm(join(ROOT, path), { recursive: true, force: true })
}
}

clean().catch((error) => {
console.error(error)
process.exitCode = 1
})
23 changes: 21 additions & 2 deletions src/StackflowApp.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { Stack } from "./stackflow";
import { useStore } from '@tanstack/react-store';
import { LayoutGroup } from 'motion/react';
import { Stack } from './stackflow';
import { MiniappWindow, MiniappStackView } from './components/ecosystem';
import { miniappRuntimeStore, miniappRuntimeSelectors, closeStackView } from './services/miniapp-runtime';
import { MiniappVisualProvider } from './services/miniapp-runtime/MiniappVisualProvider';

export function StackflowApp() {
return <Stack />;
const isStackViewOpen = useStore(miniappRuntimeStore, miniappRuntimeSelectors.isStackViewOpen);

return (
<MiniappVisualProvider>
<LayoutGroup id="miniapp-shared-layout">
<>
<Stack />
{/* 小程序窗口 - 全局 Popover 层 */}
<MiniappWindow />
{/* 层叠视图 - 多应用管理 */}
<MiniappStackView visible={isStackViewOpen} onClose={closeStackView} />
</>
</LayoutGroup>
</MiniappVisualProvider>
);
}
Loading