Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
303 changes: 303 additions & 0 deletions CHAT.md

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions docs/white-book/10-生态篇/03-小程序开发.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,45 @@ body {
"permissions": ["bio_requestAccounts"] // 只声明需要的
}
```

## 启动屏 (Splash Screen)

KeyApp 为小程序提供启动屏功能,在小程序加载时显示品牌 Logo 和加载动画。

### 配置

在 `manifest.json` 中启用:

```json
{
"icon": "icon.svg",
"themeColor": "from-blue-800 via-indigo-900 to-purple-950",
"themeColorFrom": "#1e40af",
"splashScreen": true
}
```

- **图标**:自动使用 `icon` 字段
- **背景色**:自动使用 `themeColorFrom` 字段(HEX 格式)
- **超时**:默认 5 秒后自动关闭

### 自定义超时

```json
{
"splashScreen": { "timeout": 3000 }
}
```

### 关闭启动屏

小程序初始化完成后,调用 `bio_closeSplashScreen` 关闭启动屏:

```typescript
// 在小程序初始化完成后调用
useEffect(() => {
window.bio?.request({ method: 'bio_closeSplashScreen' })
}, [])
```

> 如果未调用 `closeSplashScreen()`,启动屏会在超时后自动关闭。
12 changes: 12 additions & 0 deletions miniapps/forge/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-vitest', '@storybook/addon-docs'],
framework: {
name: '@storybook/react-vite',
options: {},
},
}

export default config
123 changes: 123 additions & 0 deletions miniapps/forge/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { Preview, ReactRenderer } from '@storybook/react-vite'
import type { DecoratorFunction } from 'storybook/internal/types'
import { useEffect } from 'react'
import { I18nextProvider } from 'react-i18next'
import i18n, { languages, defaultLanguage, getLanguageDirection, type LanguageCode } from '../src/i18n'
import '../src/index.css'

const mobileViewports = {
iPhoneSE: {
name: 'iPhone SE',
styles: { width: '375px', height: '667px' },
type: 'mobile' as const,
},
iPhone13: {
name: 'iPhone 13',
styles: { width: '390px', height: '844px' },
type: 'mobile' as const,
},
iPhone13ProMax: {
name: 'iPhone 13 Pro Max',
styles: { width: '428px', height: '926px' },
type: 'mobile' as const,
},
}

const preview: Preview = {
parameters: {
viewport: {
viewports: mobileViewports,
defaultViewport: 'iPhone13',
},
backgrounds: {
disable: true,
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
globalTypes: {
locale: {
name: 'Locale',
description: 'Language',
defaultValue: defaultLanguage,
toolbar: {
icon: 'globe',
items: Object.entries(languages).map(([code, config]) => ({
value: code,
title: `${config.name} (${config.dir.toUpperCase()})`,
right: config.dir === 'rtl' ? '←' : '→',
})),
dynamicTitle: true,
},
},
theme: {
name: 'Theme',
description: 'Color theme',
defaultValue: 'light',
toolbar: {
icon: 'paintbrush',
items: [
{ value: 'light', title: 'Light', icon: 'sun' },
{ value: 'dark', title: 'Dark', icon: 'moon' },
],
dynamicTitle: true,
},
},
},
decorators: [
// i18n + Theme decorator
((Story, context) => {
const locale = (context.globals['locale'] || defaultLanguage) as LanguageCode
const theme = context.globals['theme'] || 'light'
const direction = getLanguageDirection(locale)

useEffect(() => {
if (i18n.language !== locale) {
i18n.changeLanguage(locale)
}
document.documentElement.lang = locale
document.documentElement.dir = direction

if (theme === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}, [locale, theme, direction])

return (
<I18nextProvider i18n={i18n}>
<Story />
</I18nextProvider>
)
}) as DecoratorFunction<ReactRenderer>,

// Container decorator with theme wrapper
((Story, context) => {
const theme = context.globals['theme'] || 'light'
const direction = getLanguageDirection((context.globals['locale'] || defaultLanguage) as LanguageCode)
const isDark = theme === 'dark'

return (
<div className={`min-h-screen bg-background text-foreground transition-colors ${isDark ? 'dark' : ''}`}>
<div
className="mx-auto"
dir={direction}
style={{
maxWidth: 428,
minHeight: '100vh',
}}
>
<Story />
</div>
</div>
)
}) as DecoratorFunction<ReactRenderer>,
],
}

export default preview
7 changes: 7 additions & 0 deletions miniapps/forge/.storybook/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { beforeAll } from 'vitest'
import { setProjectAnnotations } from '@storybook/react-vite'
import * as previewAnnotations from './preview'

const annotations = setProjectAnnotations([previewAnnotations])

beforeAll(annotations.beforeAll)
4 changes: 3 additions & 1 deletion miniapps/forge/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@
"publishedAt": "2024-12-15",
"updatedAt": "2024-12-28",
"beta": true,
"themeColor": "from-red-800 via-orange-900 to-amber-950"
"themeColor": "from-red-800 via-orange-900 to-amber-950",
"themeColorFrom": "#991b1b",
"splashScreen": true
}
14 changes: 12 additions & 2 deletions miniapps/forge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"typecheck": "tsc --noEmit",
"typecheck:run": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run",
"test:storybook": "echo 'Miniapp has no storybook'",
"test:run": "vitest run --project=unit",
"test:storybook": "vitest run --project=storybook",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"e2e": "bun scripts/e2e.ts",
"e2e:run": "bun scripts/e2e.ts",
"e2e:update": "bun scripts/e2e.ts --update-snapshots",
Expand Down Expand Up @@ -45,15 +47,23 @@
"@biochain/i18n-tools": "workspace:*",
"@biochain/theme-tools": "workspace:*",
"@playwright/test": "^1.49.1",
"@storybook/addon-docs": "^10.1.4",
"@storybook/addon-vitest": "^10.1.4",
"@storybook/react": "^10.1.4",
"@storybook/react-vite": "^10.1.4",
"@tailwindcss/vite": "^4.1.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.0",
"@vitest/browser": "^4.0.15",
"@vitest/browser-playwright": "^4.0.15",
"eslint-plugin-i18next": "^6.1.3",
"jsdom": "^26.1.0",
"oxlint": "^1.32.0",
"playwright": "^1.57.0",
"storybook": "^10.1.4",
"tailwindcss": "^4.1.0",
"typescript": "^5.9.3",
"vite": "^7.3.0",
Expand Down
5 changes: 5 additions & 0 deletions miniapps/forge/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ export default function App() {
const [error, setError] = useState<string | null>(null)
const [pickerOpen, setPickerOpen] = useState<'from' | 'to' | null>(null)

// 关闭启动屏
useEffect(() => {
window.bio?.request({ method: 'bio_closeSplashScreen' })
}, [])

useEffect(() => {
if (fromAmount && parseFloat(fromAmount) > 0) {
const rate = EXCHANGE_RATES[`${fromToken.symbol}-${toToken.symbol}`] || 1
Expand Down
53 changes: 53 additions & 0 deletions miniapps/forge/src/components/FireButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { FireButton } from './FireButton'
import { Zap } from 'lucide-react'

const meta = {
title: 'Components/FireButton',
component: FireButton,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof FireButton>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
children: '连接钱包',
},
}

export const WithIcon: Story = {
args: {
children: (
<>
<Zap className="size-4" />
<span>开始锻造</span>
</>
),
},
}

export const Disabled: Story = {
args: {
children: '处理中...',
disabled: true,
},
}

export const Wide: Story = {
args: {
children: '确认交易',
className: 'max-w-xs',
},
decorators: [
(Story) => (
<div className="w-80">
<Story />
</div>
),
],
}
47 changes: 40 additions & 7 deletions miniapps/forge/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
import { playwright } from '@vitest/browser-playwright'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const dirname = path.dirname(fileURLToPath(import.meta.url))

export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.test.{ts,tsx}'],
setupFiles: ['./src/test-setup.ts'],
projects: [
// 单元测试项目 (jsdom)
{
extends: './vite.config.ts',
test: {
name: 'unit',
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test-setup.ts'],
include: ['src/**/*.test.{ts,tsx}'],
exclude: ['src/**/*.stories.test.{ts,tsx}'],
},
},
// Storybook 组件测试项目 (真实浏览器)
{
extends: './vite.config.ts',
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
storybookScript: 'pnpm storybook --ci',
}),
],
test: {
name: 'storybook',
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: 'chromium' }],
},
setupFiles: ['./.storybook/vitest.setup.ts'],
},
},
],
},
})
12 changes: 12 additions & 0 deletions miniapps/teleport/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-vitest', '@storybook/addon-docs'],
framework: {
name: '@storybook/react-vite',
options: {},
},
}

export default config
Loading