feat: 表单保存优化、照片保存相册及布局性能改进

- 物品表单底部新增冻结保存按钮,保存成功显示 Toast 提示
- 照片全屏预览时支持一键保存到本地相册(expo-media-library ~16.0.0)
- 左侧标记点列表侧栏宽度从 120 调整为 160
- FoldableLayout 改用 LayoutAnimation 替代 JS 线程宽度动画,修复 Android 地图渲染空白及卡顿问题
- 修复 ImagePicker 废弃 API MediaTypeOptions.Images → MediaType.Images

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
lanrtop 2026-04-10 00:12:42 +09:00
parent eab08d8c96
commit cea2a5d208
19 changed files with 207 additions and 58 deletions

View File

@ -11,28 +11,28 @@ status: done
## 任务卡
### 📋 平面图占位容器与坐标系
### 🔵 平面图占位容器与坐标系
- status: done
- complexity: M
- files: apps/property-survey/src/features/map/FloorMap.tsx
- depends: survey-app-shell
- acceptance: 渲染一个带灰色背景的矩形占位区域作为平面图,使用 onLayout 获取实际像素尺寸并转换相对坐标0~1容器宽高比固定为 4:3适配折叠/展开两种宽度
### 📋 长按手势添加标记点
### 🔵 长按手势添加标记点
- status: done
- complexity: M
- files: apps/property-survey/src/features/map/useMarkerGesture.ts
- depends: floor-map
- acceptance: 在平面图区域长按 500ms 后触发,将触点位置转换为相对坐标并调用 insertMarker 写入数据库,新标记点立即在地图上显示,不误触短按事件
### 📋 标记点图标组件
### 🔵 标记点图标组件
- status: done
- complexity: M
- files: apps/property-survey/src/features/map/MapMarker.tsx
- depends: floor-map
- acceptance: 在平面图上按相对坐标绝对定位渲染图钉图标,已有物品的标记点显示蓝色图钉 + 物品数量角标,空标记点显示灰色图钉,点击触发 onPress 回调
### 📋 当前楼层标记点数据加载
### 🔵 当前楼层标记点数据加载
- status: done
- complexity: S
- files: apps/property-survey/src/features/map/MarkerList.tsx

View File

@ -11,21 +11,21 @@ status: done
## 任务卡
### 📋 汇总数据查询 Hook
### 🔵 汇总数据查询 Hook
- status: done
- complexity: M
- files: apps/property-survey/src/features/summary/useSummaryData.ts
- depends: local-data-layer
- acceptance: 查询所有 items JOIN markers JOIN photos返回带楼层、坐标、物品名、数量、单价、备注、照片数的扁平列表支持按 floor1/2/3/全部)和 name 关键字过滤;数据写入后调用 refresh() 触发重新查询
### 📋 汇总列表与筛选栏
### 🔵 汇总列表与筛选栏
- status: done
- complexity: M
- files: apps/property-survey/src/features/summary/SummaryList.tsx, apps/property-survey/src/features/summary/FilterBar.tsx
- depends: inventory-summary
- acceptance: 列表每行显示楼层标签、物品名、数量、单价、备注1FilterBar 提供楼层选择器1F/2F/3F/全部和物品名搜索框滚动性能流畅FlatList空数据时显示引导文案
### 📋 统计汇总卡片
### 🔵 统计汇总卡片
- status: done
- complexity: S
- files: apps/property-survey/src/features/summary/SummaryStats.tsx

View File

@ -11,21 +11,21 @@ status: done
## 任务卡
### 📋 底部抽屉/右栏容器
### 🔵 底部抽屉/右栏容器
- status: done
- complexity: M
- files: apps/property-survey/src/features/item/ItemFormSheet.tsx
- depends: survey-app-shell, floor-map
- acceptance: 折叠时从屏幕底部滑出BottomSheet展开时在 FoldableLayout 右栏内渲染;接受 markerId 和 onClose 两个 prop顶部显示标记点位置名称和所属楼层
### 📋 物品字段表单
### 🔵 物品字段表单
- status: done
- complexity: M
- files: apps/property-survey/src/features/item/ItemFields.tsx, apps/property-survey/src/features/item/useItemForm.ts
- depends: item-form
- acceptance: 包含物品名称必填文本、数量数字键盘、单价数字键盘可选、备注1多行文本、备注2多行文本五个字段保存时校验物品名称非空useItemForm 管理字段状态和 insertItem / updateItem 调用
### 📋 标记点下物品列表
### 🔵 标记点下物品列表
- status: done
- complexity: M
- files: apps/property-survey/src/features/item/ItemList.tsx

View File

@ -11,27 +11,27 @@ status: done
## 任务卡
### 📋 SQLite 初始化与 Schema 建表
### 🔵 SQLite 初始化与 Schema 建表
- status: done
- complexity: M
- files: apps/property-survey/src/db/database.ts, apps/property-survey/src/db/schema.ts
- acceptance: App 启动时自动执行 CREATE TABLE IF NOT EXISTS建立 markers / items / photos 三张表,重复启动不报错,字段与设计文档一致
### 📋 标记点 CRUD 操作
### 🔵 标记点 CRUD 操作
- status: done
- complexity: M
- files: apps/property-survey/src/db/markers.ts
- depends: local-data-layer
- acceptance: 提供 insertMarker / getMarkersByFloor / updateMarker / deleteMarker 四个函数deleteMarker 级联删除关联 items 和 photos所有函数返回类型安全的 Promise
### 📋 物品记录 CRUD 操作
### 🔵 物品记录 CRUD 操作
- status: done
- complexity: M
- files: apps/property-survey/src/db/items.ts
- depends: local-data-layer
- acceptance: 提供 insertItem / getItemsByMarker / updateItem / deleteItem 四个函数,支持批量查询(传入 marker_id 数组),返回带关联照片数量的扩展类型
### 📋 照片路径 CRUD 操作
### 🔵 照片路径 CRUD 操作
- status: done
- complexity: S
- files: apps/property-survey/src/db/photos.ts

View File

@ -11,14 +11,14 @@ status: done
## 任务卡
### 📋 摄像头调用与图片压缩
### 🔵 摄像头调用与图片压缩
- status: done
- complexity: M
- files: apps/property-survey/src/features/photo/usePhotoCapture.ts
- depends: local-data-layer
- acceptance: 使用 expo-image-picker 提供「拍照」和「从相册选择」两种入口;拍摄/选择后自动压缩至最大宽度 1080px、JPEG quality 0.75;将压缩后的文件复制到 expo-file-system 的 App 专属目录;调用 insertPhoto 写入数据库;全流程有权限检查,拒绝权限时给出提示
### 📋 照片网格预览与删除
### 🔵 照片网格预览与删除
- status: done
- complexity: M
- files: apps/property-survey/src/features/photo/PhotoGrid.tsx, apps/property-survey/src/features/photo/PhotoPicker.tsx

View File

@ -11,27 +11,27 @@ React Native (Expo SDK 52 + Expo Router) 应用骨架,实现 Z Fold 6 折叠/
## 任务卡
### 📋 Expo 应用骨架初始化
### 🔵 Expo 应用骨架初始化
- status: done
- complexity: M
- files: apps/property-survey/app.json, apps/property-survey/package.json, apps/property-survey/app/_layout.tsx, apps/property-survey/app/index.tsx, apps/property-survey/app/summary.tsx
- depends: local-data-layer
- acceptance: `pnpm --filter property-survey start` 能在 Expo Go 或开发构建中启动底部导航可在「登记」和「汇总」两个页面间切换SQLite 数据库在启动时完成初始化
### 📋 折叠屏状态感知 Hook
### 🔵 折叠屏状态感知 Hook
- status: done
- complexity: S
- files: apps/property-survey/src/layout/useFoldState.ts
- acceptance: useFoldState 返回 `{ isUnfolded: boolean, windowWidth: number }`,以 720px 为断点区分折叠/展开状态useWindowDimensions 变化时自动更新,在模拟器宽度调整时可观察到状态切换
### 📋 双栏/单栏自适应布局容器
### 🔵 双栏/单栏自适应布局容器
- status: done
- complexity: M
- files: apps/property-survey/src/layout/FoldableLayout.tsx
- depends: survey-app-shell
- acceptance: 展开时渲染左右两栏(左侧 flexGrow:1 地图区,右侧 380px 详情区),折叠时只渲染左侧(地图全屏),接受 `left``right` 两个 ReactNode 插槽
### 📋 楼层切换标签组件
### 🔵 楼层切换标签组件
- status: done
- complexity: S
- files: apps/property-survey/src/layout/FloorTabs.tsx

View File

@ -2,7 +2,7 @@
"devices": [
{
"installationId": "626cc0bf-3d4a-47d1-9fd9-50b31e88db04",
"lastUsed": 1775741394159
"lastUsed": 1775747314027
}
]
}

View File

@ -6,9 +6,9 @@ export * from 'expo-router';
declare module 'expo-router' {
export namespace ExpoRouter {
export interface __routes<T extends string | object = string> {
hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/settings`; params?: Router.UnknownInputParams; } | { pathname: `/summary`; params?: Router.UnknownInputParams; } | { pathname: `/../src/features/item/ItemFormSheet`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; };
hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/`; params?: Router.UnknownOutputParams; } | { pathname: `/settings`; params?: Router.UnknownOutputParams; } | { pathname: `/summary`; params?: Router.UnknownOutputParams; } | { pathname: `/../src/features/item/ItemFormSheet`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; };
href: Router.RelativePathString | Router.ExternalPathString | `/${`?${string}` | `#${string}` | ''}` | `/settings${`?${string}` | `#${string}` | ''}` | `/summary${`?${string}` | `#${string}` | ''}` | `/../src/features/item/ItemFormSheet${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/settings`; params?: Router.UnknownInputParams; } | { pathname: `/summary`; params?: Router.UnknownInputParams; } | { pathname: `/../src/features/item/ItemFormSheet`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; };
hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/settings`; params?: Router.UnknownInputParams; } | { pathname: `/summary`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; };
hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/`; params?: Router.UnknownOutputParams; } | { pathname: `/settings`; params?: Router.UnknownOutputParams; } | { pathname: `/summary`; params?: Router.UnknownOutputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; };
href: Router.RelativePathString | Router.ExternalPathString | `/${`?${string}` | `#${string}` | ''}` | `/settings${`?${string}` | `#${string}` | ''}` | `/summary${`?${string}` | `#${string}` | ''}` | `/_sitemap${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/`; params?: Router.UnknownInputParams; } | { pathname: `/settings`; params?: Router.UnknownInputParams; } | { pathname: `/summary`; params?: Router.UnknownInputParams; } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; };
}
}
}

View File

@ -25,6 +25,14 @@
"package": "com.example.propertysurvey"
},
"plugins": [
[
"expo-media-library",
{
"photosPermission": "允许访问相册以保存照片",
"savePhotosPermission": "允许保存照片到相册",
"isAccessMediaLocationEnabled": true
}
],
[
"expo-build-properties",
{

View File

@ -20,6 +20,7 @@
"expo-file-system": "~18.0.0",
"expo-image-picker": "~16.0.0",
"expo-linking": "^7.0.5",
"expo-media-library": "~16.0.0",
"expo-router": "~4.0.0",
"expo-sharing": "~12.0.0",
"expo-sqlite": "~15.1.0",

View File

@ -20,7 +20,7 @@ interface ItemFieldsProps {
}
export function ItemFields({ hook }: ItemFieldsProps) {
const { form, setField, nameError, photoItemId } = hook
const { form, setField, nameError, photoItemId, save } = hook
const { categories } = useCategories()
return (
@ -147,6 +147,13 @@ export function ItemFields({ hook }: ItemFieldsProps) {
<View style={styles.bottomSpacer} />
</ScrollView>
{/* 冻结在底部的保存按钮 */}
<View style={styles.saveBar}>
<TouchableOpacity style={styles.saveBtn} onPress={save} activeOpacity={0.8}>
<Text style={styles.saveBtnText}></Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
{/* 分隔线 */}
@ -226,7 +233,22 @@ const styles = StyleSheet.create({
errorText: { fontSize: 12, color: '#EF4444', marginLeft: 4 },
bottomSpacer: { height: 24 },
bottomSpacer: { height: 8 },
saveBar: {
paddingHorizontal: 14,
paddingVertical: 10,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#E5E7EB',
backgroundColor: '#fff',
},
saveBtn: {
backgroundColor: '#10B981',
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center',
},
saveBtnText: { color: '#fff', fontSize: 14, fontWeight: '700' },
// 分隔线
colDivider: { width: StyleSheet.hairlineWidth, backgroundColor: '#E5E7EB' },

View File

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Alert, Animated, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { Ionicons } from '@expo/vector-icons'
import type { Marker } from '@repo/types'
import { copyMarker, deleteMarker, updateMarker } from '../../db/markers'
@ -20,7 +20,22 @@ export function ItemFormSheet({ marker, onClose, onMarkerUpdated }: ItemFormShee
const [refreshKey, setRefreshKey] = useState(0)
const [currentColor, setCurrentColor] = useState(marker?.color ?? '#3B82F6')
const handleSaved = useCallback(() => setRefreshKey((k) => k + 1), [])
const toastOpacity = useRef(new Animated.Value(0)).current
const showToast = useCallback(() => {
toastOpacity.setValue(0)
Animated.sequence([
Animated.timing(toastOpacity, { toValue: 1, duration: 150, useNativeDriver: true }),
Animated.delay(1400),
Animated.timing(toastOpacity, { toValue: 0, duration: 300, useNativeDriver: true }),
]).start()
}, [toastOpacity])
const handleSaved = useCallback(() => {
setRefreshKey((k) => k + 1)
onMarkerUpdated?.()
showToast()
}, [onMarkerUpdated, showToast])
const handleCopy = useCallback(async () => {
if (!marker) return
@ -128,6 +143,11 @@ export function ItemFormSheet({ marker, onClose, onMarkerUpdated }: ItemFormShee
<View style={styles.formPane}>
<ItemFields hook={hook} />
</View>
<Animated.View style={[styles.toast, { opacity: toastOpacity }]} pointerEvents="none">
<Ionicons name="checkmark-circle" size={16} color="#fff" />
<Text style={styles.toastText}></Text>
</Animated.View>
</View>
)
}
@ -163,4 +183,18 @@ const styles = StyleSheet.create({
colorDot: { width: 24, height: 24, borderRadius: 12 },
colorDotSelected: { borderWidth: 3, borderColor: '#111827' },
formPane: { flex: 1 },
toast: {
position: 'absolute',
bottom: 64,
alignSelf: 'center',
flexDirection: 'row',
alignItems: 'center',
gap: 6,
backgroundColor: '#111827',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
},
toastText: { color: '#fff', fontSize: 13, fontWeight: '600' },
})

View File

@ -29,15 +29,18 @@ export function useItemForm(markerId: number, onSaved: () => void) {
const [editingId, setEditingId] = useState<number | null>(null)
const [photoItemId, setPhotoItemId] = useState<number | null>(null)
const [isDirty, setIsDirty] = useState(false)
const [nameError, setNameError] = useState('')
function setField<K extends keyof FormState>(key: K, value: FormState[K]) {
setForm((prev) => ({ ...prev, [key]: value }))
setIsDirty(true)
if (key === 'name') setNameError('')
}
function startEdit(item: Item) {
setEditingId(item.id)
setPhotoItemId(item.id)
setNameError('')
setForm({
name: item.name,
category: item.category,
@ -56,6 +59,7 @@ export function useItemForm(markerId: number, onSaved: () => void) {
setPhotoItemId(null)
setForm(DEFAULT_FORM)
setIsDirty(false)
setNameError('')
}
// 自动保存:有名称且有改动时才执行,不弹错误
@ -89,5 +93,37 @@ export function useItemForm(markerId: number, onSaved: () => void) {
}
}, [form, editingId, isDirty, markerId, onSaved])
return { form, setField, isDirty, editingId, photoItemId, startEdit, resetForm, autoSave }
// 手动保存:有名称校验,无论 isDirty 均执行
const save = useCallback(async () => {
if (!form.name.trim()) {
setNameError('物品名称不能为空')
return
}
const params: InsertItemParams = {
markerId,
name: form.name.trim(),
category: form.category,
quantity: Math.max(1, parseInt(form.quantity, 10) || 1),
unit: form.unit.trim(),
spec: form.spec.trim(),
estimatedValue: parseFloat(form.estimatedValue) || 0,
referencePrice: parseFloat(form.referencePrice) || 0,
remark: form.remark.trim(),
}
try {
if (editingId !== null) {
await updateItem(editingId, params)
} else {
const saved = await insertItem(params)
setEditingId(saved.id)
setPhotoItemId(saved.id)
}
setIsDirty(false)
onSaved()
} catch (err) {
console.error('保存物品失败:', err)
}
}, [form, editingId, markerId, onSaved])
return { form, setField, nameError, isDirty, editingId, photoItemId, startEdit, resetForm, autoSave, save }
}

View File

@ -146,7 +146,7 @@ const styles = StyleSheet.create({
// ── 侧栏 ──────────────────────────────────────────
sidebar: {
width: 120,
width: 160,
borderRightWidth: StyleSheet.hairlineWidth,
borderRightColor: '#E5E7EB',
backgroundColor: '#FAFAFA',

View File

@ -10,6 +10,7 @@ import {
View,
} from 'react-native'
import { Ionicons } from '@expo/vector-icons'
import * as MediaLibrary from 'expo-media-library'
import type { Photo } from '@repo/types'
import { deletePhoto, getPhotosByItem } from '../../db/photos'
import { PhotoPicker } from './PhotoPicker'
@ -21,6 +22,7 @@ interface PhotoGridProps {
export function PhotoGrid({ itemId }: PhotoGridProps) {
const [photos, setPhotos] = useState<Photo[]>([])
const [previewIndex, setPreviewIndex] = useState<number | null>(null)
const [saving, setSaving] = useState(false)
const load = useCallback(async () => {
try {
@ -53,6 +55,27 @@ export function PhotoGrid({ itemId }: PhotoGridProps) {
])
}
const handleSaveToAlbum = useCallback(async () => {
const currentPhoto = previewIndex !== null ? photos[previewIndex] : null
if (!currentPhoto) return
setSaving(true)
try {
const { status } = await MediaLibrary.requestPermissionsAsync()
if (status !== 'granted') {
Alert.alert('权限不足', '请在系统设置中允许访问相册')
return
}
await MediaLibrary.saveToLibraryAsync(currentPhoto.filePath)
Alert.alert('已保存', '照片已成功保存到本地相册')
} catch (err) {
console.error('保存到相册失败:', err)
Alert.alert('保存失败', '无法保存照片,请重试')
} finally {
setSaving(false)
}
}, [previewIndex, photos])
const currentPhoto = previewIndex !== null ? photos[previewIndex] : null
return (
@ -80,9 +103,20 @@ export function PhotoGrid({ itemId }: PhotoGridProps) {
onRequestClose={() => setPreviewIndex(null)}
>
<View style={styles.overlay}>
{/* 关闭按钮 */}
<TouchableOpacity style={styles.closeBtn} onPress={() => setPreviewIndex(null)}>
<Ionicons name="close" size={28} color="#fff" />
</TouchableOpacity>
{/* 保存到相册按钮 */}
<TouchableOpacity
style={[styles.saveBtn, saving && styles.saveBtnDisabled]}
onPress={handleSaveToAlbum}
disabled={saving}
>
<Ionicons name="download-outline" size={22} color="#fff" />
</TouchableOpacity>
<ScrollView
horizontal
pagingEnabled
@ -132,6 +166,14 @@ const styles = StyleSheet.create({
zIndex: 10,
padding: 4,
},
saveBtn: {
position: 'absolute',
top: 48,
left: 16,
zIndex: 10,
padding: 4,
},
saveBtnDisabled: { opacity: 0.4 },
previewPage: {
width: 400,
justifyContent: 'center',

View File

@ -43,7 +43,7 @@ export function usePhotoCapture(itemId: number, onAdded: () => void) {
}
const pickOptions: ImagePicker.ImagePickerOptions = {
mediaTypes: ImagePicker.MediaTypeOptions.Images,
mediaTypes: ImagePicker.MediaType.Images,
allowsEditing: false,
quality: JPEG_QUALITY,
exif: false,

View File

@ -1,7 +1,12 @@
import { type ReactNode, useEffect, useRef } from 'react'
import { Animated, StyleSheet, View } from 'react-native'
import { type ReactNode, useRef } from 'react'
import { LayoutAnimation, Platform, StyleSheet, UIManager, View } from 'react-native'
import { useFoldState } from './useFoldState'
// Android 需要手动开启 LayoutAnimation 支持
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental?.(true)
}
const DETAIL_PANEL_WIDTH = 260
interface FoldableLayoutProps {
@ -12,15 +17,13 @@ interface FoldableLayoutProps {
export function FoldableLayout({ left, right, showDetail = false }: FoldableLayoutProps) {
const { isUnfolded } = useFoldState()
const panelWidth = useRef(new Animated.Value(showDetail ? DETAIL_PANEL_WIDTH : 0)).current
useEffect(() => {
Animated.timing(panelWidth, {
toValue: showDetail ? DETAIL_PANEL_WIDTH : 0,
duration: 220,
useNativeDriver: false,
}).start()
}, [showDetail])
// 在 props 变化引起的 re-render 之前触发原生布局动画
const prevShowDetail = useRef(showDetail)
if (prevShowDetail.current !== showDetail) {
prevShowDetail.current = showDetail
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
}
if (!isUnfolded) {
return <View style={styles.single}>{left}</View>
@ -29,25 +32,16 @@ export function FoldableLayout({ left, right, showDetail = false }: FoldableLayo
return (
<View style={styles.dual}>
<View style={styles.mapPane}>{left}</View>
<Animated.View style={[styles.detailPane, { width: panelWidth }]}>
{right}
</Animated.View>
{showDetail && (
<View style={styles.detailPane}>{right}</View>
)}
</View>
)
}
const styles = StyleSheet.create({
single: {
flex: 1,
},
dual: {
flex: 1,
flexDirection: 'row',
},
mapPane: {
flexGrow: 1,
},
detailPane: {
overflow: 'hidden',
},
single: { flex: 1 },
dual: { flex: 1, flexDirection: 'row' },
mapPane: { flexGrow: 1 },
detailPane: { width: DETAIL_PANEL_WIDTH },
})

12
pnpm-lock.yaml generated
View File

@ -40,6 +40,9 @@ importers:
expo-linking:
specifier: ^7.0.5
version: 7.0.5(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)
expo-media-library:
specifier: ~16.0.0
version: 16.0.5(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1))
expo-router:
specifier: ~4.0.0
version: 4.0.22(2up5cuuap7dudmj6aangd7ypjy)
@ -2146,6 +2149,11 @@ packages:
peerDependencies:
expo: '*'
expo-media-library@16.0.5:
resolution: {integrity: sha512-O9RUqBWgJVRF0mO6EiLSBFyfb5wR1/ZqovbT43V0TAo5sgcjrHRs+0NID/U6BWDRuiFeX2AU516JgNDutNUFSw==}
peerDependencies:
expo: '*'
expo-modules-autolinking@2.0.8:
resolution: {integrity: sha512-DezgnEYFQYic8hKGhkbztBA3QUmSftjaNDIKNAtS2iGJmzCcNIkatjN2slFDSWjSTNo8gOvPQyMKfyHWFvLpOQ==}
hasBin: true
@ -6787,6 +6795,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
expo-media-library@16.0.5(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)):
dependencies:
expo: 52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1)))(react-native@0.76.3(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.3.1))(react@18.3.1)
expo-modules-autolinking@2.0.8:
dependencies:
'@expo/spawn-async': 1.7.2