diff --git a/apps/mobile-shell/App.tsx b/apps/mobile-shell/App.tsx
index 910c907a..3925d79c 100644
--- a/apps/mobile-shell/App.tsx
+++ b/apps/mobile-shell/App.tsx
@@ -7,8 +7,8 @@ import {
Linking,
Platform,
StyleSheet,
- View,
} from 'react-native';
+import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import type { WebViewMessageEvent } from 'react-native-webview';
import { WebView } from 'react-native-webview';
@@ -24,6 +24,7 @@ import {
shouldOpenInMobileShellWebView,
} from './src/mobileShellNavigation';
import { subscribeMobileNetworkStatus } from './src/mobileShellNetwork';
+import { MOBILE_SHELL_SAFE_AREA_EDGES } from './src/mobileShellSafeArea';
import { buildMobileShellUrl } from './src/mobileShellUrl';
const defaultWebUrl = 'http://127.0.0.1:3000/';
@@ -165,32 +166,34 @@ export default function App() {
};
return (
-
-
- {
- setCanGoBack(event.canGoBack);
- webViewRef.current?.injectJavaScript(
- buildHostBridgeMessageScript({
- bridge: 'GenarrativeHostBridge',
- version: 1,
- event: 'navigation.canGoBack',
- payload: {
- canGoBack: event.canGoBack,
- },
- }),
- );
- }}
- setSupportMultipleWindows={false}
- />
-
+
+
+
+ {
+ setCanGoBack(event.canGoBack);
+ webViewRef.current?.injectJavaScript(
+ buildHostBridgeMessageScript({
+ bridge: 'GenarrativeHostBridge',
+ version: 1,
+ event: 'navigation.canGoBack',
+ payload: {
+ canGoBack: event.canGoBack,
+ },
+ }),
+ );
+ }}
+ setSupportMultipleWindows={false}
+ />
+
+
);
}
diff --git a/apps/mobile-shell/package.json b/apps/mobile-shell/package.json
index a082ee21..da1de514 100644
--- a/apps/mobile-shell/package.json
+++ b/apps/mobile-shell/package.json
@@ -26,6 +26,7 @@
"expo-status-bar": "^56.0.4",
"react": "^19.0.0",
"react-native": "^0.86.0",
+ "react-native-safe-area-context": "^5.8.0",
"react-native-webview": "^13.16.1"
},
"devDependencies": {
diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs
index a2f26495..c5a49abf 100644
--- a/apps/mobile-shell/scripts/check-config.mjs
+++ b/apps/mobile-shell/scripts/check-config.mjs
@@ -124,6 +124,9 @@ for (const snippet of [
'subscribeMobileNetworkStatus',
'navigation.canGoBack',
'buildHostBridgeMessageScript',
+ 'SafeAreaProvider',
+ 'SafeAreaView',
+ 'MOBILE_SHELL_SAFE_AREA_EDGES',
]) {
if (!appSource.includes(snippet)) {
throw new Error(`mobile shell App missing ${snippet}`);
@@ -137,6 +140,7 @@ for (const dependency of [
'expo-network',
'expo-notifications',
'expo-sharing',
+ 'react-native-safe-area-context',
]) {
if (!packageConfig.dependencies?.[dependency]) {
throw new Error(`mobile shell package missing ${dependency}`);
diff --git a/apps/mobile-shell/src/mobileShellSafeArea.test.ts b/apps/mobile-shell/src/mobileShellSafeArea.test.ts
new file mode 100644
index 00000000..65bda42a
--- /dev/null
+++ b/apps/mobile-shell/src/mobileShellSafeArea.test.ts
@@ -0,0 +1,14 @@
+import { describe, expect, test } from 'vitest';
+
+import { MOBILE_SHELL_SAFE_AREA_EDGES } from './mobileShellSafeArea';
+
+describe('mobile shell safe area', () => {
+ test('protects the WebView from every device edge', () => {
+ expect(MOBILE_SHELL_SAFE_AREA_EDGES).toEqual([
+ 'top',
+ 'right',
+ 'bottom',
+ 'left',
+ ]);
+ });
+});
diff --git a/apps/mobile-shell/src/mobileShellSafeArea.ts b/apps/mobile-shell/src/mobileShellSafeArea.ts
new file mode 100644
index 00000000..1430cad4
--- /dev/null
+++ b/apps/mobile-shell/src/mobileShellSafeArea.ts
@@ -0,0 +1,6 @@
+export const MOBILE_SHELL_SAFE_AREA_EDGES = [
+ 'top',
+ 'right',
+ 'bottom',
+ 'left',
+] as const;
diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md
index b7b86585..0a0b00e7 100644
--- a/docs/project-memory/shared-memory/decision-log.md
+++ b/docs/project-memory/shared-memory/decision-log.md
@@ -38,6 +38,7 @@
- 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。
- 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则,成功回传 `action=captured`,不暴露设备本地 URI,也不请求麦克风权限;Tauri 壳不声明该能力,不伪造桌面拍摄。
- 2026-06-18 H5 图片上传接入宿主导入:`CreativeImageInputPanel` 在 `native_app` 且声明 `file.importImage` / `file.captureImage` 时,主图上传和描述参考图上传可分别调用 `importHostImageFile()` / `captureHostImageFile()`,并把宿主返回的 base64 图片转换为现有 `File` 回调;浏览器、小程序和未声明能力的裁剪壳继续走原生 `` 路径,不新增玩法侧上传分叉。
+- 2026-06-18 移动壳安全区:Expo 壳根布局使用 `react-native-safe-area-context` 的 `SafeAreaProvider` 与四边 `SafeAreaView` 保护 WebView,避免 H5 主站内容贴进 iOS 刘海、底部 Home Indicator、Android 状态栏或横屏边缘;该能力属于宿主壳布局保护,不新增 H5 占位 UI,不改变玩法 runtime 或 HostBridge capability。
- 2026-06-18 桌面图片拖入接入主图槽位:`CreativeImageInputPanel` 在桌面壳声明 `file.imageDropped` 时订阅宿主拖入事件,只在拖入坐标命中当前主图卡片且未被上层元素遮挡时消费事件,避免窗口级拖入被多个创作面板同时接收;成功后仍转换为现有 `File` 上传回调。
- 2026-06-18 H5 背景音乐接入宿主生命周期:`useBackgroundMusic` 通过 `useHostLifecycleActive()` 消费 `subscribeHostAppLifecycle()` 的归一结果,宿主进入后台、inactive 或桌面窗口失焦时降低音量并暂停音频循环,同时 `suspend` WebAudio context;回到 `active + focused` 且用户原本开启音乐时再恢复播放,不改变用户音量设置。
- 2026-06-18 固定玩法音频接入宿主生命周期:前端新增 `useHostLifecycleActive()` 统一消费 `subscribeHostAppLifecycle()`,`useBackgroundMusic`、拼图运行态和抓大鹅运行态都只依赖该归一状态判断音频可播放性;宿主 inactive、background 或窗口失焦时暂停 `