Build RoomScan AI starter app

This commit is contained in:
Ismail Ali
2026-04-28 11:43:56 +02:00
parent 306c93fb47
commit bdc0c97431
16 changed files with 1071 additions and 18 deletions

1
.gitignore vendored
View File

@@ -31,6 +31,7 @@ yarn-error.*
*.pem *.pem
# local env files # local env files
.env
.env*.local .env*.local
# typescript # typescript

21
App.tsx
View File

@@ -1,20 +1,13 @@
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { AppNavigator } from './src/navigation/AppNavigator';
export default function App() { export default function App() {
return ( return (
<View style={styles.container}> <SafeAreaProvider>
<Text>Open up App.tsx to start working on your app!</Text> <AppNavigator />
<StatusBar style="auto" /> <StatusBar style="light" />
</View> </SafeAreaProvider>
); );
} }
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

122
README.md Normal file
View File

@@ -0,0 +1,122 @@
# RoomScan AI
RoomScan AI ist ein Starterprojekt fuer eine skalierbare iOS App zur Erstellung von 3D Raumscans. Die App soll Nutzer Schritt fuer Schritt durch einen Raumscan fuehren, Hinweise zu Geschwindigkeit, Richtung, Startpunkt und Fehlern anzeigen und am Ende ein 3D Raum-Modell visualisieren.
Die erste Version ist bewusst als React Native und Expo TypeScript Projekt aufgebaut. Sie nutzt nur notwendige Basis- und Navigationspakete, damit spaeter per `expo prebuild` sauber in ein natives iOS/Xcode-Projekt migriert werden kann.
## Features
- HomeScreen mit Projektziel und Scan-Ablauf
- ScanScreen mit Mock-Kameraansicht
- Instructions Overlay fuer gefuehrte Hinweise wie `Langsamer bewegen` oder `Richtung ändern`
- Ergebnis Screen mit Mock 3D Ansicht
- Stack Navigation mit TypeScript-Typen
- Vorbereitete Ordnerstruktur fuer Komponenten, Screens, Navigation, Services und Theme
## Tech Stack
- React Native
- Expo
- TypeScript
- React Navigation
- iOS-Zielplattform mit spaeterer Xcode-Erweiterung
- Geplante native Integration: ARKit und RoomPlan
## Projektstruktur
```text
roomscan-ai/
├── App.tsx
├── app.json
├── package.json
├── src/
│ ├── components/
│ │ ├── InstructionOverlay.tsx
│ │ ├── PrimaryButton.tsx
│ │ └── ScanProgressCard.tsx
│ ├── navigation/
│ │ ├── AppNavigator.tsx
│ │ └── types.ts
│ ├── screens/
│ │ ├── HomeScreen.tsx
│ │ ├── ResultScreen.tsx
│ │ └── ScanScreen.tsx
│ ├── services/
│ │ └── scanGuidance.ts
│ └── theme/
│ └── colors.ts
└── README.md
```
## Installation
Voraussetzungen:
- Node.js
- npm
- Expo CLI ueber `npx expo`
- Fuer native iOS Builds spaeter: macOS, Xcode und Apple Developer Account
Abhaengigkeiten installieren:
```bash
npm install
```
Entwicklungsserver starten:
```bash
npm run start
```
iOS Preview starten:
```bash
npm run ios
```
Hinweis: Auf Windows kann `npm run ios` kein lokales iOS-Simulator-Build starten. Fuer echte iOS-Entwicklung wird macOS mit Xcode benoetigt.
## Expo Prebuild und Xcode Vorbereitung
Das Projekt ist so vorbereitet, dass spaeter ein natives iOS-Projekt erzeugt werden kann:
```bash
npm run prebuild:ios
```
Dabei wird der Ordner `ios/` generiert. Dieser Ordner ist aktuell in `.gitignore` eingetragen, weil native Dateien erst versioniert werden sollten, wenn die RoomPlan-Integration konkret umgesetzt wird.
Wichtige iOS-Vorbereitungen in `app.json`:
- `bundleIdentifier`: `com.roomscanai.app`
- `NSCameraUsageDescription` fuer die spaetere Kamera- und RoomPlan-Nutzung
- `supportsTablet`: aktiviert fuer iPad-Kompatibilitaet
## RoomPlan Integration
RoomPlan ist eine native iOS-Technologie von Apple und wird nicht direkt durch Expo Go bereitgestellt. Die geplante Integration erfolgt spaeter ueber ein natives iOS-Modul oder eine Xcode-Erweiterung nach `expo prebuild`.
Geplanter Integrationspfad:
- Expo App als UI- und Navigationsbasis beibehalten
- iOS-Projekt per `expo prebuild --platform ios` erzeugen
- Native RoomPlan Capture Session in Swift implementieren
- Scan-Daten an React Native uebergeben
- Ergebnisdaten als 3D Modell oder strukturierte Raumdaten anzeigen
## Roadmap
- Mock UI fuer Scan Flow stabilisieren
- Gefuehrte Scan-Hinweise als State Machine modellieren
- Fehler- und Qualitaetsstatus fuer Scans erweitern
- Persistenz fuer Scan-Ergebnisse vorbereiten
- Native RoomPlan Integration in iOS/Xcode umsetzen
- 3D Ergebnisansicht mit echten RoomPlan-Daten anbinden
- Exportoptionen fuer Raumdaten pruefen
- TestFlight Build vorbereiten
- iOS App Store Veröffentlichung vorbereiten
## Ziel
Ziel ist eine saubere, erweiterbare iOS App fuer gefuehrte 3D Raumscans mit einem klaren Pfad zur nativen ARKit- und RoomPlan-Integration und anschliessender Veröffentlichung im iOS App Store.

View File

@@ -2,6 +2,7 @@
"expo": { "expo": {
"name": "roomscan-ai", "name": "roomscan-ai",
"slug": "roomscan-ai", "slug": "roomscan-ai",
"description": "Eine iOS App zur Erstellung von 3D Raumscans mit ARKit und RoomPlan.",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
@@ -13,7 +14,11 @@
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true,
"bundleIdentifier": "com.roomscanai.app",
"infoPlist": {
"NSCameraUsageDescription": "RoomScan AI benoetigt die Kamera, um spaeter 3D Raumscans mit RoomPlan und ARKit zu erstellen."
}
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {

309
package-lock.json generated
View File

@@ -8,10 +8,14 @@
"name": "roomscan-ai", "name": "roomscan-ai",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@react-navigation/native": "^7.2.2",
"@react-navigation/native-stack": "^7.14.12",
"expo": "~54.0.33", "expo": "~54.0.33",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5" "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
@@ -2964,6 +2968,123 @@
"integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-navigation/core": {
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.2.tgz",
"integrity": "sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==",
"license": "MIT",
"dependencies": {
"@react-navigation/routers": "^7.5.3",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.3.11",
"query-string": "^7.1.3",
"react-is": "^19.1.0",
"use-latest-callback": "^0.2.4",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
"react": ">= 18.2.0"
}
},
"node_modules/@react-navigation/core/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@react-navigation/core/node_modules/react-is": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz",
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
"license": "MIT"
},
"node_modules/@react-navigation/elements": {
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.15.tgz",
"integrity": "sha512-cyz/pPiyyC6gaTVLsGFc1g0MYgrmuCFqklAWGXMWPscr5YU3ui94vPI4vnZwcsEy0T758TQWLzmS5XudZeRKcA==",
"license": "MIT",
"dependencies": {
"color": "^4.2.3",
"use-latest-callback": "^0.2.4",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
"@react-native-masked-view/masked-view": ">= 0.2.0",
"@react-navigation/native": "^7.2.2",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 4.0.0"
},
"peerDependenciesMeta": {
"@react-native-masked-view/masked-view": {
"optional": true
}
}
},
"node_modules/@react-navigation/native": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz",
"integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==",
"license": "MIT",
"dependencies": {
"@react-navigation/core": "^7.17.2",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.3.11",
"use-latest-callback": "^0.2.4"
},
"peerDependencies": {
"react": ">= 18.2.0",
"react-native": "*"
}
},
"node_modules/@react-navigation/native-stack": {
"version": "7.14.12",
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.12.tgz",
"integrity": "sha512-dUfpkrVeVKKV8iqXsmoUp3Rv0iH3YaB3eZwScru/FlcqAp/r3/qA6zEXkGX9hZK+/ziWAPFrf1frBSNbgOYSFQ==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^2.9.15",
"color": "^4.2.3",
"sf-symbols-typescript": "^2.1.0",
"warn-once": "^0.1.1"
},
"peerDependencies": {
"@react-navigation/native": "^7.2.2",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 4.0.0",
"react-native-screens": ">= 4.0.0"
}
},
"node_modules/@react-navigation/native/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@react-navigation/routers": {
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz",
"integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==",
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.10", "version": "0.27.10",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
@@ -3915,6 +4036,19 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3930,6 +4064,34 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/commander": { "node_modules/commander": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -4086,6 +4248,15 @@
} }
} }
}, },
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"license": "MIT",
"engines": {
"node": ">=0.10"
}
},
"node_modules/deep-extend": { "node_modules/deep-extend": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -4907,6 +5078,12 @@
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -4934,6 +5111,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -5279,6 +5465,12 @@
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
}, },
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -7226,6 +7418,24 @@
"qrcode-terminal": "bin/qrcode-terminal.js" "qrcode-terminal": "bin/qrcode-terminal.js"
} }
}, },
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/queue": { "node_modules/queue": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -7278,6 +7488,18 @@
"ws": "^7" "ws": "^7"
} }
}, },
"node_modules/react-freeze": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
"integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=17.0.0"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -7351,6 +7573,31 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-safe-area-context": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-screens": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
"license": "MIT",
"dependencies": {
"react-freeze": "^1.0.0",
"react-native-is-edge-to-edge": "^1.2.1",
"warn-once": "^0.1.0"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native/node_modules/@react-native/virtualized-lists": { "node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.81.5", "version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
@@ -7827,6 +8074,15 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/sf-symbols-typescript": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz",
"integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7877,6 +8133,15 @@
"plist": "^3.0.5" "plist": "^3.0.5"
} }
}, },
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -7938,6 +8203,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/sprintf-js": { "node_modules/sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -8001,6 +8275,15 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -8490,6 +8773,24 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/use-latest-callback": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz",
"integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -8541,6 +8842,12 @@
"makeerror": "1.0.12" "makeerror": "1.0.12"
} }
}, },
"node_modules/warn-once": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz",
"integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==",
"license": "MIT"
},
"node_modules/wcwidth": { "node_modules/wcwidth": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",

View File

@@ -6,13 +6,18 @@
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web" "web": "expo start --web",
"prebuild:ios": "expo prebuild --platform ios"
}, },
"dependencies": { "dependencies": {
"@react-navigation/native": "^7.2.2",
"@react-navigation/native-stack": "^7.14.12",
"expo": "~54.0.33", "expo": "~54.0.33",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5" "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",

View File

@@ -0,0 +1,54 @@
import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../theme/colors';
type InstructionOverlayProps = {
title: string;
detail: string;
tone?: 'info' | 'warning' | 'danger';
};
export function InstructionOverlay({ title, detail, tone = 'info' }: InstructionOverlayProps) {
const toneColor = tone === 'warning' ? colors.warning : tone === 'danger' ? colors.danger : colors.primary;
return (
<View style={[styles.container, { borderColor: toneColor }]}>
<View style={[styles.indicator, { backgroundColor: toneColor }]} />
<View style={styles.textBlock}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.detail}>{detail}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
backgroundColor: 'rgba(8, 17, 31, 0.92)',
borderRadius: 20,
borderWidth: 1,
flexDirection: 'row',
gap: 14,
padding: 16,
},
indicator: {
borderRadius: 999,
height: 14,
width: 14,
},
textBlock: {
flex: 1,
},
title: {
color: colors.textPrimary,
fontSize: 16,
fontWeight: '800',
},
detail: {
color: colors.textSecondary,
fontSize: 13,
lineHeight: 19,
marginTop: 4,
},
});

View File

@@ -0,0 +1,51 @@
import { Pressable, StyleSheet, Text } from 'react-native';
import { colors } from '../theme/colors';
type PrimaryButtonProps = {
label: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
};
export function PrimaryButton({ label, onPress, variant = 'primary' }: PrimaryButtonProps) {
return (
<Pressable
accessibilityRole="button"
onPress={onPress}
style={({ pressed }) => [
styles.button,
variant === 'secondary' && styles.secondary,
pressed && styles.pressed,
]}
>
<Text style={[styles.label, variant === 'secondary' && styles.secondaryLabel]}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
alignItems: 'center',
backgroundColor: colors.primary,
borderRadius: 18,
paddingHorizontal: 22,
paddingVertical: 16,
},
secondary: {
backgroundColor: colors.surfaceRaised,
borderColor: colors.border,
borderWidth: 1,
},
pressed: {
opacity: 0.82,
},
label: {
color: colors.background,
fontSize: 16,
fontWeight: '800',
},
secondaryLabel: {
color: colors.textPrimary,
},
});

View File

@@ -0,0 +1,62 @@
import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../theme/colors';
type ScanProgressCardProps = {
progress: number;
};
export function ScanProgressCard({ progress }: ScanProgressCardProps) {
return (
<View style={styles.card}>
<View style={styles.row}>
<Text style={styles.label}>Scan-Fortschritt</Text>
<Text style={styles.value}>{progress}%</Text>
</View>
<View style={styles.track}>
<View style={[styles.fill, { width: `${progress}%` }]} />
</View>
<Text style={styles.hint}>Halte das iPhone ruhig und bewege dich entlang der Wandkontur.</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 22,
borderWidth: 1,
padding: 18,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
label: {
color: colors.textSecondary,
fontSize: 14,
},
value: {
color: colors.textPrimary,
fontSize: 18,
fontWeight: '800',
},
track: {
backgroundColor: colors.surfaceRaised,
borderRadius: 999,
height: 10,
marginTop: 14,
overflow: 'hidden',
},
fill: {
backgroundColor: colors.accent,
height: '100%',
},
hint: {
color: colors.textSecondary,
fontSize: 13,
lineHeight: 19,
marginTop: 14,
},
});

View File

@@ -0,0 +1,29 @@
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { HomeScreen } from '../screens/HomeScreen';
import { ResultScreen } from '../screens/ResultScreen';
import { ScanScreen } from '../screens/ScanScreen';
import { colors } from '../theme/colors';
import type { RootStackParamList } from './types';
const Stack = createNativeStackNavigator<RootStackParamList>();
export function AppNavigator() {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: colors.background },
headerTintColor: colors.textPrimary,
headerTitleStyle: { fontWeight: '700' },
contentStyle: { backgroundColor: colors.background },
}}
>
<Stack.Screen name="Home" component={HomeScreen} options={{ title: 'RoomScan AI' }} />
<Stack.Screen name="Scan" component={ScanScreen} options={{ title: 'Raum scannen' }} />
<Stack.Screen name="Result" component={ResultScreen} options={{ title: '3D Ergebnis' }} />
</Stack.Navigator>
</NavigationContainer>
);
}

5
src/navigation/types.ts Normal file
View File

@@ -0,0 +1,5 @@
export type RootStackParamList = {
Home: undefined;
Scan: undefined;
Result: undefined;
};

108
src/screens/HomeScreen.tsx Normal file
View File

@@ -0,0 +1,108 @@
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { PrimaryButton } from '../components/PrimaryButton';
import type { RootStackParamList } from '../navigation/types';
import { colors } from '../theme/colors';
type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
const steps = ['Startpunkt wählen', 'Langsam entlang der Wand bewegen', 'Hinweise beachten', '3D Modell prüfen'];
export function HomeScreen({ navigation }: HomeScreenProps) {
return (
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.hero}>
<Text style={styles.eyebrow}>ARKit + RoomPlan vorbereitet</Text>
<Text style={styles.title}>Geführte 3D Raumscans fuer iOS</Text>
<Text style={styles.subtitle}>
RoomScan AI fuehrt Nutzer Schritt fuer Schritt durch den Scan und bereitet die App auf eine
native RoomPlan-Integration per Expo Prebuild vor.
</Text>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>Scan Ablauf</Text>
{steps.map((step, index) => (
<View key={step} style={styles.stepRow}>
<Text style={styles.stepNumber}>{index + 1}</Text>
<Text style={styles.stepText}>{step}</Text>
</View>
))}
</View>
<PrimaryButton label="Mock Scan starten" onPress={() => navigation.navigate('Scan')} />
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
content: {
gap: 24,
padding: 24,
},
hero: {
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 28,
borderWidth: 1,
padding: 24,
},
eyebrow: {
color: colors.primary,
fontSize: 13,
fontWeight: '800',
letterSpacing: 0.5,
textTransform: 'uppercase',
},
title: {
color: colors.textPrimary,
fontSize: 34,
fontWeight: '900',
lineHeight: 39,
marginTop: 12,
},
subtitle: {
color: colors.textSecondary,
fontSize: 16,
lineHeight: 24,
marginTop: 14,
},
card: {
backgroundColor: colors.surfaceRaised,
borderRadius: 24,
padding: 20,
},
cardTitle: {
color: colors.textPrimary,
fontSize: 18,
fontWeight: '800',
marginBottom: 14,
},
stepRow: {
alignItems: 'center',
flexDirection: 'row',
gap: 12,
paddingVertical: 9,
},
stepNumber: {
backgroundColor: colors.primaryDark,
borderRadius: 999,
color: colors.textPrimary,
fontWeight: '800',
overflow: 'hidden',
paddingHorizontal: 10,
paddingVertical: 6,
},
stepText: {
color: colors.textSecondary,
flex: 1,
fontSize: 15,
},
});

View File

@@ -0,0 +1,130 @@
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { PrimaryButton } from '../components/PrimaryButton';
import type { RootStackParamList } from '../navigation/types';
import { colors } from '../theme/colors';
type ResultScreenProps = NativeStackScreenProps<RootStackParamList, 'Result'>;
export function ResultScreen({ navigation }: ResultScreenProps) {
return (
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
<View style={styles.container}>
<View style={styles.modelPreview}>
<View style={styles.floorPlan}>
<View style={styles.wallLong} />
<View style={styles.wallShort} />
<View style={styles.roomBlockLarge} />
<View style={styles.roomBlockSmall} />
</View>
<Text style={styles.previewLabel}>Mock 3D Modell</Text>
</View>
<View style={styles.summaryCard}>
<Text style={styles.title}>Scan bereit zur Auswertung</Text>
<Text style={styles.body}>
Diese Ansicht ist ein Platzhalter fuer das spaetere RoomPlan-Ergebnis. Nach der nativen iOS
Integration werden hier erkannte Waende, Tueren, Fenster und Moebel angezeigt.
</Text>
</View>
<View style={styles.actions}>
<PrimaryButton label="Neuen Scan starten" onPress={() => navigation.navigate('Scan')} />
<PrimaryButton label="Zur Startseite" variant="secondary" onPress={() => navigation.navigate('Home')} />
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
container: {
flex: 1,
gap: 20,
padding: 20,
},
modelPreview: {
alignItems: 'center',
backgroundColor: colors.surface,
borderColor: colors.border,
borderRadius: 30,
borderWidth: 1,
flex: 1,
justifyContent: 'center',
minHeight: 360,
padding: 24,
},
floorPlan: {
borderColor: colors.primary,
borderRadius: 24,
borderWidth: 3,
height: 220,
transform: [{ rotateX: '58deg' }, { rotateZ: '-18deg' }],
width: 220,
},
wallLong: {
backgroundColor: colors.primary,
borderRadius: 10,
height: 14,
left: 20,
position: 'absolute',
top: 72,
width: 150,
},
wallShort: {
backgroundColor: colors.accent,
borderRadius: 10,
height: 110,
position: 'absolute',
right: 48,
top: 54,
width: 14,
},
roomBlockLarge: {
backgroundColor: 'rgba(98, 214, 255, 0.24)',
borderRadius: 18,
bottom: 28,
height: 70,
left: 34,
position: 'absolute',
width: 88,
},
roomBlockSmall: {
backgroundColor: 'rgba(181, 240, 109, 0.28)',
borderRadius: 14,
height: 52,
position: 'absolute',
right: 32,
top: 24,
width: 58,
},
previewLabel: {
color: colors.textSecondary,
fontSize: 15,
marginTop: 32,
},
summaryCard: {
backgroundColor: colors.surfaceRaised,
borderRadius: 24,
padding: 20,
},
title: {
color: colors.textPrimary,
fontSize: 21,
fontWeight: '900',
},
body: {
color: colors.textSecondary,
fontSize: 15,
lineHeight: 23,
marginTop: 10,
},
actions: {
gap: 12,
},
});

135
src/screens/ScanScreen.tsx Normal file
View File

@@ -0,0 +1,135 @@
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { InstructionOverlay } from '../components/InstructionOverlay';
import { PrimaryButton } from '../components/PrimaryButton';
import { ScanProgressCard } from '../components/ScanProgressCard';
import type { RootStackParamList } from '../navigation/types';
import { getGuidanceForProgress } from '../services/scanGuidance';
import { colors } from '../theme/colors';
type ScanScreenProps = NativeStackScreenProps<RootStackParamList, 'Scan'>;
export function ScanScreen({ navigation }: ScanScreenProps) {
const [progress, setProgress] = useState(10);
const guidance = getGuidanceForProgress(progress);
// Simulates RoomPlan scan updates until native iOS capture is connected.
useEffect(() => {
const intervalId = setInterval(() => {
setProgress((current) => Math.min(current + 5, 92));
}, 1200);
return () => clearInterval(intervalId);
}, []);
return (
<SafeAreaView style={styles.safeArea} edges={['bottom']}>
<View style={styles.container}>
<View style={styles.cameraMock}>
<View style={styles.gridLineVertical} />
<View style={styles.gridLineHorizontal} />
<View style={styles.scanFrame}>
<Text style={styles.roomLabel}>Wohnzimmer</Text>
<View style={styles.cornerMarkerTop} />
<View style={styles.cornerMarkerBottom} />
</View>
<View style={styles.overlayPosition}>
<InstructionOverlay title={guidance.title} detail={guidance.detail} tone={guidance.tone} />
</View>
</View>
<ScanProgressCard progress={progress} />
<View style={styles.actions}>
<PrimaryButton label="Scan abschliessen" onPress={() => navigation.navigate('Result')} />
<PrimaryButton label="Neu starten" variant="secondary" onPress={() => setProgress(10)} />
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
},
container: {
flex: 1,
gap: 18,
padding: 20,
},
cameraMock: {
backgroundColor: '#0b1628',
borderColor: colors.border,
borderRadius: 30,
borderWidth: 1,
flex: 1,
minHeight: 380,
overflow: 'hidden',
},
gridLineVertical: {
backgroundColor: 'rgba(98, 214, 255, 0.16)',
height: '100%',
left: '50%',
position: 'absolute',
width: 1,
},
gridLineHorizontal: {
backgroundColor: 'rgba(98, 214, 255, 0.16)',
height: 1,
position: 'absolute',
top: '50%',
width: '100%',
},
scanFrame: {
borderColor: colors.primary,
borderRadius: 24,
borderWidth: 2,
bottom: 70,
left: 34,
position: 'absolute',
right: 34,
top: 70,
},
roomLabel: {
alignSelf: 'center',
backgroundColor: colors.primary,
borderRadius: 999,
color: colors.background,
fontWeight: '900',
marginTop: -16,
overflow: 'hidden',
paddingHorizontal: 16,
paddingVertical: 8,
},
cornerMarkerTop: {
backgroundColor: colors.accent,
borderRadius: 8,
height: 16,
position: 'absolute',
right: 20,
top: 42,
width: 16,
},
cornerMarkerBottom: {
backgroundColor: colors.warning,
borderRadius: 8,
bottom: 34,
height: 16,
left: 24,
position: 'absolute',
width: 16,
},
overlayPosition: {
bottom: 18,
left: 18,
position: 'absolute',
right: 18,
},
actions: {
gap: 12,
},
});

View File

@@ -0,0 +1,33 @@
export type ScanGuidance = {
title: string;
detail: string;
tone: 'info' | 'warning' | 'danger';
};
const guidanceSteps: ScanGuidance[] = [
{
title: 'Startpunkt setzen',
detail: 'Richte die Kamera auf eine freie Ecke und beginne dort mit dem Raumscan.',
tone: 'info',
},
{
title: 'Langsamer bewegen',
detail: 'Die Kamera verliert Details. Reduziere die Geschwindigkeit fuer stabilere Messpunkte.',
tone: 'warning',
},
{
title: 'Richtung ändern',
detail: 'Schwenke leicht nach rechts, damit die Wandkante vollstaendig erkannt wird.',
tone: 'info',
},
{
title: 'Bereich erneut erfassen',
detail: 'Ein Objekt wurde unklar erkannt. Gehe einen Schritt zurueck und scanne die Zone erneut.',
tone: 'danger',
},
];
export function getGuidanceForProgress(progress: number) {
const index = Math.min(Math.floor(progress / 25), guidanceSteps.length - 1);
return guidanceSteps[index];
}

13
src/theme/colors.ts Normal file
View File

@@ -0,0 +1,13 @@
export const colors = {
background: '#08111f',
surface: '#101d31',
surfaceRaised: '#17263d',
primary: '#62d6ff',
primaryDark: '#0f8fbf',
accent: '#b5f06d',
warning: '#ffcf5a',
danger: '#ff7a7a',
textPrimary: '#f4f8ff',
textSecondary: '#a7b5c9',
border: '#263854',
};