diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 000000000..38b8bf71b
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,102 @@
+name: Android CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+concurrency:
+ group: build-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Build with Gradle
+ run: ./gradlew build
+
+ - name: Archive production artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: app
+ path: app/build/outputs/apk/
+
+ test:
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4.1.0
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3.13.0
+ with:
+ distribution: 'adopt'
+ java-version: '17'
+
+ - name: Grant execute permissions for gradlew
+ run: chmod +x ./gradlew
+
+ - name: Run Tests with Gradle
+ run: ./gradlew test
+
+ lint:
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4.1.0
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3.13.0
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ cache: gradle
+
+ - name: Grant execute permissions for gradlew
+ run: chmod +x ./gradlew
+ - name: Run Lint with Gradle
+ run: ./gradlew lint
+ - name: Upload lint results
+ uses: actions/upload-artifact@v2
+ with:
+ name: lint-results
+ path: '**/build/reports/lint-results-*.html'
+
+ dokka:
+ runs-on: ubuntu-latest
+ needs: [ build, test ]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4.1.0
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3.13.0
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ cache: gradle
+
+ - name: Grant execute permissions for gradlew
+ run: chmod +x ./gradlew
+ - name: Run Dokka with Gradle
+ run: ./gradlew dokkaHtmlMultiModule
+
+ - name: Deploy to GitHub Pages
+ uses: JamesIves/github-pages-deploy-action@v4
+ with:
+ branch: gh-pages
+ folder: build/dokka/htmlMultiModule
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 524f0963b..c4b932d84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Compiled class file
*.class
+*.dex
# Log file
*.log
@@ -22,3 +23,33 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
+
+# IDEA/Android Studio project files
+*.iml
+.idea/*
+!.idea/copyright
+# Keep the code styles.
+!/.idea/codeStyles
+/.idea/codeStyles/*
+!/.idea/codeStyles/Project.xml
+!/.idea/codeStyles/codeStyleConfig.xml
+
+# Gradle cache
+.gradle
+local.properties
+
+# Android Studio captures folder
+captures/
+
+# generated files
+bin/
+gen/
+out/
+build/
+
+# track build-logic module
+!**/edu/stanford/spezikt/build
+
+# built application files
+*.apk
+*.ap_
\ No newline at end of file
diff --git a/README.md b/README.md
index 70dd742cf..6709c9b41 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,14 @@
+[![Android CI](https://github.com/Basler182/Spezi/actions/workflows/android.yml/badge.svg)](https://github.com/StanfordSpezi/SpeziKt/actions/workflows/android.yml)
+[![pages-build-deployment](https://github.com/Basler182/SpeziKt/actions/workflows/pages/pages-build-deployment/badge.svg?branch=gh-pages)](https://github.com/Basler182/SpeziKt/actions/workflows/pages/pages-build-deployment)
+
# SpeziKt
Kotlin & Android Version of the Stanford Spezi Framework
+
+### An Ecosystem of Modules
+
+SpeziKt is a collection of modules that can be used to build Android applications
+
+### Modules
+
+- **Design System**: Provides a cohesive user interface and user experience
+ components. [Read More](./core/design/README.md)
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/README.md b/app/README.md
new file mode 100644
index 000000000..2ca59e1f4
--- /dev/null
+++ b/app/README.md
@@ -0,0 +1,4 @@
+# Module app
+
+The Template App is an integral part of the Stanford SpeziKt Framework and is intended to show how
+to use the Framework. The simple app shows the use of the SpeziKt modules and the core elements.
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 000000000..0da00402f
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,69 @@
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.jetbrainsKotlinAndroid)
+}
+
+android {
+ namespace = "edu.stanford.spezikt"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ applicationId = "edu.stanford.spezikt"
+ minSdk = libs.versions.minSdk.get().toInt()
+ targetSdk = libs.versions.targetSdk.get().toInt()
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExtensionVersion.get()
+ }
+ packaging {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+
+ implementation(project(":core:design"))
+
+ testImplementation(libs.junit)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/edu/stanford/spezikt/ExampleInstrumentedTest.kt b/app/src/androidTest/java/edu/stanford/spezikt/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..880ccb50c
--- /dev/null
+++ b/app/src/androidTest/java/edu/stanford/spezikt/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package edu.stanford.spezikt
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("edu.stanford.spezikt", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..92e598f9c
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/edu/stanford/spezikt/MainActivity.kt b/app/src/main/java/edu/stanford/spezikt/MainActivity.kt
new file mode 100644
index 000000000..4102743de
--- /dev/null
+++ b/app/src/main/java/edu/stanford/spezikt/MainActivity.kt
@@ -0,0 +1,46 @@
+package edu.stanford.spezikt
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import edu.stanford.spezikt.core.design.theme.SpeziKtTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ SpeziKtTheme {
+ // A surface container using the 'background' color from the theme
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ Greeting("SpeziKt")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun Greeting(name: String, modifier: Modifier = Modifier) {
+ Text(
+ text = "Hello $name!",
+ modifier = modifier
+ )
+}
+
+@Preview(showBackground = true)
+@Composable
+fun GreetingPreview() {
+ SpeziKtTheme {
+ Greeting("SpeziKt")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 000000000..6f3b755bf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..5443b18e2
--- /dev/null
+++ b/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,4 @@
+
+
+ SpeziKt
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..a830ce377
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+
+
+ #FFFFFFFF
+ #FF8C1515
+ #FF820000
+ #FFB83A4B
+ #FF53565A
+ #FF2E2D29
+ #FF43423E
+ #FF767674
+ #FFABABA9
+ #FFD5D5D4
+ #FFEAEAEA
+ #FFEBF2FC
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..15ed0e11f
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ SpeziKt
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..45f970a8d
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 000000000..fa0f996d2
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..9ee9997b0
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/edu/stanford/spezikt/ExampleUnitTest.kt b/app/src/test/java/edu/stanford/spezikt/ExampleUnitTest.kt
new file mode 100644
index 000000000..14ac2da27
--- /dev/null
+++ b/app/src/test/java/edu/stanford/spezikt/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package edu.stanford.spezikt
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build-logic/README.md b/build-logic/README.md
new file mode 100644
index 000000000..e404064d9
--- /dev/null
+++ b/build-logic/README.md
@@ -0,0 +1,44 @@
+# Module Convention Plugins
+
+## Overview
+
+The `build-logic` folder contains SpeziKt specific convention plugins that are used for common
+module configurations.
+
+## Features
+
+- **Standardization**: Ensures that all library modules follow a specific convention, which improves
+ consistency across this projects.
+- **Improved reusability**: The clear separation and modularization of the build logic promotes the
+ reusability of code within the project or across
+ projects [(idiomatic-gradle)](https://github.com/jjohannes/idiomatic-gradle).
+- **Reduced complexity and cognitive load**: Convention plugins significantly simplify build scripts
+ by encapsulating commonly used conventions and configurations. This reduces the complexity of
+ individual build scripts and reduces the cognitive load on developers, making it easier to
+ understand and manage the build
+ process [(square)](https://developer.squareup.com/blog/herding-elephants/).
+- **Improved build performance**: Unlike buildSrc, which is compiled and checked on every build,
+ convention plugins can be precompiled and treated like any other dependency. This avoids the
+ performance penalty of recompiling the build logic with each build and leads to faster build
+ times, especially in large
+ projects [(square)](https://developer.squareup.com/blog/herding-elephants/).
+- **Increased modularity and isolation**: By using convention plugins, the build logic can be
+ modularized and isolated from the rest of the build script. This allows for cleaner code
+ management and reduces the risk of bugs propagating through the build script. It also makes it
+ easier to test the build logic [(square)](https://developer.squareup.com/blog/herding-elephants/).
+
+## Usage
+
+To apply a convention plugin, add the following to your `build.gradle.kts`:
+
+```kotlin
+plugins {
+ id("spezikt.android.application.compose")
+}
+```
+
+## Plugins
+
+Current list of convention plugins:
+
+- [`spezikt.android.library.compose`](convention/src/main/kotlin/SpeziLibraryComposeConventionPlugin.kt)
diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
new file mode 100644
index 000000000..64eb03a66
--- /dev/null
+++ b/build-logic/convention/build.gradle.kts
@@ -0,0 +1,39 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ `kotlin-dsl`
+}
+
+group = "edu.stanford.spezikt.build.logic"
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
+tasks.withType().configureEach {
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+}
+
+dependencies {
+ compileOnly(libs.android.gradlePlugin)
+ compileOnly(libs.android.tools.common)
+ compileOnly(libs.kotlin.gradlePlugin)
+}
+
+tasks {
+ validatePlugins {
+ enableStricterValidation = true
+ failOnWarning = true
+ }
+}
+
+gradlePlugin {
+ plugins {
+ register("speziLibraryComposeConventionPlugin") {
+ id = "spezikt.android.library.compose"
+ implementationClass = "SpeziLibraryComposeConventionPlugin"
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/SpeziLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/SpeziLibraryComposeConventionPlugin.kt
new file mode 100644
index 000000000..53559cfa8
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/SpeziLibraryComposeConventionPlugin.kt
@@ -0,0 +1,24 @@
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import com.android.build.gradle.LibraryExtension
+import edu.stanford.spezikt.build.logic.convention.configureAndroidCompose
+import edu.stanford.spezikt.build.logic.convention.disableUnnecessaryAndroidTests
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.getByType
+
+class SpeziLibraryComposeConventionPlugin : Plugin {
+ override fun apply(project: Project) {
+ with(project) {
+ plugins.apply("com.android.library")
+ plugins.apply("org.jetbrains.kotlin.android")
+ val libraryExtension = extensions.getByType()
+
+ configureAndroidCompose(libraryExtension)
+
+ extensions.configure {
+ disableUnnecessaryAndroidTests(project)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/Configuration.kt b/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/Configuration.kt
new file mode 100644
index 000000000..6b479fd79
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/Configuration.kt
@@ -0,0 +1,87 @@
+package edu.stanford.spezikt.build.logic.convention
+
+import com.android.build.api.dsl.CommonExtension
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import org.gradle.api.JavaVersion
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.provideDelegate
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+
+/**
+ * Configures the SpeziKt Android Library Compose plugin for the project.
+ */
+internal fun Project.configureAndroidCompose(
+ commonExtension: CommonExtension<*, *, *, *, *, *>,
+) {
+ commonExtension.apply {
+ compileSdk = findVersion("compileSdk").toInt()
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = findVersion("kotlinCompilerExtensionVersion")
+ }
+
+ defaultConfig {
+ minSdk = findVersion("minSdk").toInt()
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ dependencies {
+ val bom = findLibrary("androidx-compose-bom")
+ implementation(platform(bom))
+ androidTestImplementation(platform(bom))
+
+ implementation(project(":core:design"))
+ implementation(findBundle("compose-implementation"))
+
+ testImplementation(findBundle("compose-testImplementation"))
+
+ androidTestImplementation(findLibrary("androidx.junit"))
+ androidTestImplementation(findLibrary("androidx.espresso.core"))
+ androidTestImplementation(findLibrary("androidx.ui.test.junit4"))
+ }
+ }
+ configureKotlin()
+}
+
+/**
+ * Configures the Kotlin compiler options for the project.
+ * The compiler options are set to use Java 17 as the target JVM version.
+ * Additionally, the compiler options are set to treat all warnings as errors.
+ */
+internal fun Project.configureKotlin() {
+ tasks.withType().configureEach {
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ val warningsAsErrors: String? by project
+ allWarningsAsErrors = warningsAsErrors.toBoolean()
+ freeCompilerArgs =
+ freeCompilerArgs + listOf("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
+ }
+ }
+}
+
+/**
+ * The purpose of this function is to optimize the build process.
+ * If there are no Android tests for a variant, there's no need to spend time
+ * and resources to build and run these non-existent tests. By disabling the tests for these variants,
+ * the build process can be faster and more efficient.
+ */
+internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
+ project: Project,
+) = beforeVariants {
+ @Suppress("UnstableApiUsage")
+ it.androidTest.enable = it.androidTest.enable
+ && project.projectDir.resolve("src/androidTest").exists()
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/DependencyHandler.kt b/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/DependencyHandler.kt
new file mode 100644
index 000000000..9a6d2cde5
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/DependencyHandler.kt
@@ -0,0 +1,15 @@
+package edu.stanford.spezikt.build.logic.convention
+
+import org.gradle.api.artifacts.dsl.DependencyHandler
+
+internal fun DependencyHandler.implementation(dependency: Any) {
+ add("implementation", dependency)
+}
+
+internal fun DependencyHandler.testImplementation(dependency: Any) {
+ add("testImplementation", dependency)
+}
+
+internal fun DependencyHandler.androidTestImplementation(dependency: Any) {
+ add("androidTestImplementation", dependency)
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/ProjectExtensions.kt
new file mode 100644
index 000000000..a5a277651
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/edu/stanford/spezikt/build/logic/convention/ProjectExtensions.kt
@@ -0,0 +1,17 @@
+package edu.stanford.spezikt.build.logic.convention
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.VersionCatalog
+import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.kotlin.dsl.getByType
+
+val Project.libs
+ get(): VersionCatalog = extensions.getByType().named("libs")
+
+internal fun Project.findBundle(name: String) = libs.findBundle(name).get()
+
+internal fun Project.findLibrary(name: String) = libs.findLibrary(name).get()
+
+internal fun Project.findVersion(name: String) = libs.findVersion(name).get().toString()
+
+internal fun Project.findPlugin(name: String) = libs.findPlugin(name).get().toString()
\ No newline at end of file
diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties
new file mode 100644
index 000000000..6977b7191
--- /dev/null
+++ b/build-logic/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
\ No newline at end of file
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
new file mode 100644
index 000000000..b8c2f72bd
--- /dev/null
+++ b/build-logic/settings.gradle.kts
@@ -0,0 +1,15 @@
+@Suppress("UnstableApiUsage")
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
+
+rootProject.name = "build-logic"
+include(":convention")
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 000000000..3b081b195
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,44 @@
+import org.jetbrains.dokka.gradle.DokkaTaskPartial
+
+plugins {
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.jetbrainsKotlinAndroid) apply false
+ alias(libs.plugins.androidLibrary) apply false
+ id("org.jetbrains.dokka") version "1.9.20"
+}
+
+subprojects {
+ apply(plugin = "org.jetbrains.dokka")
+
+
+ if (this != rootProject) {
+ rootProject.tasks.named("dokkaHtmlMultiModule") {
+ dependsOn("${project.path}:dokkaHtml")
+ }
+ }
+
+ tasks.withType().configureEach {
+ dokkaSourceSets.configureEach {
+ noAndroidSdkLink.set(false)
+ skipDeprecated.set(true)
+ skipEmptyPackages.set(true)
+ includeNonPublic.set(false)
+ jdkVersion.set(17)
+ if (file("README.md").exists()) {
+ includes.from("README.md")
+ }
+ }
+ }
+
+ val dokkaHtmlMultiModule = tasks.findByName("dokkaHtmlMultiModule") ?: tasks.create(
+ "dokkaHtmlMultiModule",
+ DokkaTaskPartial::class.java
+ )
+ rootProject.tasks.named("dokkaHtmlMultiModule") {
+ dependsOn(dokkaHtmlMultiModule)
+ }
+}
+
+tasks.dokkaHtmlMultiModule {
+ moduleName.set("SpeziKt Documentation")
+}
\ No newline at end of file
diff --git a/core/design/.gitignore b/core/design/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/core/design/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/design/README.md b/core/design/README.md
new file mode 100644
index 000000000..898eb0f1f
--- /dev/null
+++ b/core/design/README.md
@@ -0,0 +1,68 @@
+# Module design
+
+## Overview
+
+The Design System Module is part of the SpeziKt framework, designed to provide
+a cohesive user interface and user experience components. It ensures consistent aesthetics and
+functionality across different parts of the application, enhancing both developer efficiency and
+user satisfaction.
+
+## Features
+
+- **Theming**: Supports light and dark modes and customizable color schemes.
+- **Components**: Includes reusable UI components such as a button optimized for accessibility and
+ ease of use.
+- **Typography**: Implements a scalable typography system that adapts to different screen sizes and
+ orientations.
+- **Icons and Graphics**: Provides a set of commonly used icons and graphics that maintain high
+ resolution and scalability across devices.
+
+## Installation
+
+To integrate the Design System Module into your project, add the following dependency to
+your `build.gradle` file:
+
+```gradle
+dependencies {
+ implementation(project(":core:design"))
+}
+```
+
+## Usage
+
+To use a component from the design system, refer to the specific documentation included in the
+module or look at the example usage below:
+
+### Theming
+
+```kotlin
+SpeziKtTheme {
+ Text(text = "Text")
+}
+```
+
+### Components
+
+```kotlin
+SpeziButton(
+ onClick = { },
+ content = { Text(text = "Text") }
+)
+```
+
+### Typography
+
+```kotlin
+Text(text = "Text", style = MaterialTheme.typography.bodyLarge)
+```
+
+### Icons and Graphics
+
+```kotlin
+ImageVector.vectorResource(R.drawable.ic_medications)
+```
+
+# Package edu.stanford.spezikt.core.design.theme
+
+The `theme` package contains the theme configuration for the Design System Module. It includes:
+Color, Theme and Typography.
\ No newline at end of file
diff --git a/core/design/build.gradle.kts b/core/design/build.gradle.kts
new file mode 100644
index 000000000..ee84c070a
--- /dev/null
+++ b/core/design/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ alias(libs.plugins.androidLibrary)
+ alias(libs.plugins.jetbrainsKotlinAndroid)
+}
+
+android {
+ namespace = "edu.stanford.spezikt.core.design"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.kotlinCompilerExtensionVersion.get()
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.material3)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui.tooling.preview)
+
+ testImplementation(libs.junit)
+ testImplementation(libs.androidx.compose.ui.test)
+
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+
+ debugImplementation(libs.androidx.ui.tooling)
+}
\ No newline at end of file
diff --git a/core/design/src/androidTest/java/edu/stanford/spezikt/core/design/ButtonKtTest.kt b/core/design/src/androidTest/java/edu/stanford/spezikt/core/design/ButtonKtTest.kt
new file mode 100644
index 000000000..834408e7f
--- /dev/null
+++ b/core/design/src/androidTest/java/edu/stanford/spezikt/core/design/ButtonKtTest.kt
@@ -0,0 +1,87 @@
+package edu.stanford.spezikt.core.design
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.isNotEnabled
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import edu.stanford.spezikt.core.design.component.SpeziButton
+import edu.stanford.spezikt.core.design.theme.SpeziKtTheme
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+
+class ButtonKtTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun speziButtonIsDisplayed() {
+ val text = "Test Button"
+ composeTestRule.setContent {
+ SpeziKtTheme {
+ SpeziButton(
+ onClick = { },
+ content = { Text(text = text) }
+ )
+ }
+ }
+
+ composeTestRule.onNodeWithText(text).assertIsDisplayed()
+ }
+
+ @Test
+ fun speziButton_isEnabled() {
+ val text = "Test Button"
+ composeTestRule.setContent {
+ SpeziKtTheme {
+ SpeziButton(
+ onClick = { /**/ },
+ content = { Text(text = text) }
+ )
+ }
+ }
+
+ composeTestRule.onNodeWithText(text).assertHasClickAction()
+ }
+
+ @Test
+ fun speziButton_isDisabled() {
+ val text = "Test Button"
+ composeTestRule.setContent {
+ SpeziKtTheme {
+ SpeziButton(
+ onClick = { },
+ enabled = false,
+ content = { Text(text = text) }
+ )
+ }
+ }
+
+ composeTestRule.onNodeWithText(text).assert(isNotEnabled())
+ }
+
+ @Test
+ fun speziButton_onClick() {
+ val text = "Test Button"
+ var clicked = false
+ composeTestRule.setContent {
+ SpeziKtTheme {
+ SpeziButton(
+ onClick = { clicked = true },
+ content = { Text(text = text) }
+ )
+ }
+ }
+
+ runBlocking {
+ composeTestRule.onNodeWithText(text).performClick()
+ }
+
+ assert(clicked)
+ }
+}
\ No newline at end of file
diff --git a/core/design/src/androidTest/java/edu/stanford/spezikt/core/design/ExampleInstrumentedTest.kt b/core/design/src/androidTest/java/edu/stanford/spezikt/core/design/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..9f2fb3d16
--- /dev/null
+++ b/core/design/src/androidTest/java/edu/stanford/spezikt/core/design/ExampleInstrumentedTest.kt
@@ -0,0 +1,22 @@
+package edu.stanford.spezikt.core.design
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("edu.stanford.spezikt.core.design.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/core/design/src/main/AndroidManifest.xml b/core/design/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/core/design/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/core/design/src/main/java/edu/stanford/spezikt/core/design/component/Button.kt b/core/design/src/main/java/edu/stanford/spezikt/core/design/component/Button.kt
new file mode 100644
index 000000000..6845ed5cc
--- /dev/null
+++ b/core/design/src/main/java/edu/stanford/spezikt/core/design/component/Button.kt
@@ -0,0 +1,60 @@
+package edu.stanford.spezikt.core.design.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import edu.stanford.spezikt.core.design.theme.SpeziKtTheme
+
+/**
+ * A button of the SpeziKt design system with customizable content.
+ *
+ * @sample edu.stanford.spezikt.core.design.component.SpeziButtonDarkPreview
+ *
+ * @param onClick The callback to be invoked when the button is clicked.
+ * @param modifier The modifier to be applied to the button.
+ * @param enabled Whether the button is enabled.
+ * @param contentPadding The padding to be applied to the button content.
+ * @param content The content of the button.
+ */
+@Composable
+fun SpeziButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
+ content: @Composable RowScope.() -> Unit,
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.onBackground,
+ ),
+ contentPadding = contentPadding,
+ content = content,
+ )
+}
+
+@Preview
+@Composable
+fun SpeziButtonDarkPreview() {
+ SpeziKtTheme {
+ Column {
+ SpeziButton(
+ onClick = { },
+ content = {
+ Text(text = "Text", style = MaterialTheme.typography.bodyLarge)
+ }
+ )
+ }
+ }
+}
+
diff --git a/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Color.kt b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Color.kt
new file mode 100644
index 000000000..e03972984
--- /dev/null
+++ b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Color.kt
@@ -0,0 +1,77 @@
+package edu.stanford.spezikt.core.design.theme
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.graphics.Color
+
+object SpeziColors {
+ val Primary
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.primary
+
+ val Secondary
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.secondary
+
+ val Tertiary
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.tertiary
+
+ val Background
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.background
+
+ val Surface
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.surface
+
+ val OnPrimary
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.onPrimary
+
+
+ val OnSecondary
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.onSecondary
+
+ val OnTertiary
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.onTertiary
+
+
+ val OnBackground
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.onBackground
+
+ val OnSurface
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.colorScheme.onSurface
+}
+
+@Suppress("unused")
+internal val CardinalRed = Color(0xFF8C1515)
+internal val CardinalRedDark = Color(0xFF820000)
+internal val CardinalRedLight = Color(0xFFB83A4B)
+
+internal val White = Color(0xFFFFFFFF)
+internal val CoolGrey = Color(0xFF53565A)
+
+internal val Black = Color(0xFF2E2D29)
+internal val Black80 = Color(0xFF43423E)
+internal val Black60 = Color(0xFF767674)
+internal val Black40 = Color(0xFFABABA9)
+internal val Black20 = Color(0xFFD5D5D4)
+internal val Black10 = Color(0xFFEAEAEA)
+
+internal val RectangleBlue = Color(0xFFEBF2FC)
\ No newline at end of file
diff --git a/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Size.kt b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Size.kt
new file mode 100644
index 000000000..3a00ece38
--- /dev/null
+++ b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Size.kt
@@ -0,0 +1,11 @@
+package edu.stanford.spezikt.core.design.theme
+
+import androidx.compose.ui.unit.dp
+
+// Spacing
+val SmallSpacing = 8.dp
+val MediumSpacing = 16.dp
+val LargeSpacing = 24.dp
+
+// Size
+val IconSize = 48.dp
\ No newline at end of file
diff --git a/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Theme.kt b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Theme.kt
new file mode 100644
index 000000000..cda1e9887
--- /dev/null
+++ b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Theme.kt
@@ -0,0 +1,91 @@
+package edu.stanford.spezikt.core.design.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
+
+typealias ComposableBlock = @Composable () -> Unit
+
+private val DarkColorScheme = darkColorScheme(
+ primary = CardinalRed,
+ secondary = Black60,
+ tertiary = CardinalRedDark,
+
+ background = Black,
+ surface = Black80,
+ onPrimary = White,
+ onSecondary = White,
+ onTertiary = White,
+ onBackground = White,
+ onSurface = White,
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = CardinalRed,
+ secondary = CoolGrey,
+ tertiary = RectangleBlue,
+
+ background = White,
+ surface = White,
+
+ onPrimary = White,
+ onSecondary = Black,
+ onTertiary = Black,
+
+ onBackground = Black,
+ onSurface = Black,
+)
+
+@Composable
+fun SpeziKtTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+ val view = LocalView.current
+ if (!view.isInEditMode) {
+ SideEffect {
+ val window = (view.context as Activity).window
+ window.statusBarColor = colorScheme.primary.toArgb()
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
+ }
+ }
+
+ val surface: ComposableBlock = {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background,
+ content = content
+ )
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = typography,
+ content = surface
+ )
+}
\ No newline at end of file
diff --git a/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Type.kt b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Type.kt
new file mode 100644
index 000000000..431032c76
--- /dev/null
+++ b/core/design/src/main/java/edu/stanford/spezikt/core/design/theme/Type.kt
@@ -0,0 +1,95 @@
+package edu.stanford.spezikt.core.design.theme
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
+import edu.stanford.spezikt.core.design.theme.SpeziTypography.bodyLarge
+import edu.stanford.spezikt.core.design.theme.SpeziTypography.labelSmall
+import edu.stanford.spezikt.core.design.theme.SpeziTypography.titleLarge
+import edu.stanford.spezikt.core.design.theme.SpeziTypography.titleSmall
+
+object SpeziTypography {
+ val bodyLarge: TextStyle
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.typography.bodyLarge
+
+ val titleLarge: TextStyle
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.typography.titleLarge
+
+ val titleSmall: TextStyle
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.typography.titleSmall
+
+ val labelSmall: TextStyle
+ @Composable
+ @ReadOnlyComposable
+ get() = MaterialTheme.typography.labelSmall
+}
+
+internal val typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ ),
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ titleSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.5.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+)
+
+@Preview(showBackground = true)
+@Composable
+private fun TypographyPreview() {
+ SpeziKtTheme {
+ Column {
+ Text(
+ text = "SpeziKtTheme.typography.titleLarge",
+ style = titleLarge
+ )
+ Text(
+ text = "SpeziKtTheme.typography.titleSmall",
+ style = titleSmall
+ )
+ Text(
+ text = "SpeziKtTheme.typography.bodyLarge",
+ style = bodyLarge
+ )
+ Text(
+ text = "SpeziKtTheme.typography.labelSmall",
+ style = labelSmall
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/design/src/main/res/drawable/ic_medication.xml b/core/design/src/main/res/drawable/ic_medication.xml
new file mode 100644
index 000000000..7b4268d76
--- /dev/null
+++ b/core/design/src/main/res/drawable/ic_medication.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/core/design/src/test/java/edu/stanford/spezikt/core/design/ExampleUnitTest.kt b/core/design/src/test/java/edu/stanford/spezikt/core/design/ExampleUnitTest.kt
new file mode 100644
index 000000000..d72e90d9c
--- /dev/null
+++ b/core/design/src/test/java/edu/stanford/spezikt/core/design/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package edu.stanford.spezikt.core.design
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..20e2a0152
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..77a113d6b
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,58 @@
+[versions]
+activityCompose = "1.9.0"
+agp = "8.4.0"
+androidTools = "31.3.2"
+appcompat = "1.6.1"
+compileSdk = "34"
+composeBom = "2024.04.01"
+coreKtx = "1.13.0"
+espressoCore = "3.5.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+kotlin = "1.9.23"
+kotlinCompilerExtensionVersion = "1.5.12"
+lifecycleRuntimeKtx = "2.7.0"
+minSdk = "29"
+mockkAgent = "1.13.10"
+mockkAndroid = "1.13.10"
+targetSdk = "34"
+truth = "1.4.2"
+uiTooling = "1.6.6"
+
+[libraries]
+android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
+android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
+mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockkAgent" }
+mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockkAndroid" }
+truth = { module = "com.google.truth:truth", version.ref = "truth" }
+ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
+
+[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+androidLibrary = { id = "com.android.library", version.ref = "agp" }
+jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+spezikt-android-library-compose = { id = "spezikt.android.library.compose", version = "unspecified" }
+
+
+[bundles]
+compose-androidTestImplementation = []
+compose-implementation = ["androidx-material3", "androidx-ui-tooling-preview", "androidx-ui-tooling", "androidx-core-ktx", "androidx-appcompat"]
+compose-testImplementation = ["junit", "androidx-compose-ui-test"]
+mockk-androidTestImplementation = ["mockk-agent", "mockk-android"]
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..e708b1c02
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..e3c7912c3
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Apr 18 21:37:46 CEST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 000000000..4f906e0c8
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..107acd32c
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 000000000..7579569fa
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,33 @@
+pluginManagement {
+ includeBuild("build-logic")
+
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ @Suppress("UnstableApiUsage")
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ @Suppress("UnstableApiUsage")
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "SpeziKt"
+
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
+include(":app")
+include(":core:design")
+include(":spezi-module:onboarding")
+include(":spezi-module:contact")
\ No newline at end of file
diff --git a/spezi-module/contact/.gitignore b/spezi-module/contact/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/spezi-module/contact/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/spezi-module/contact/README.md b/spezi-module/contact/README.md
new file mode 100644
index 000000000..0f236ef47
--- /dev/null
+++ b/spezi-module/contact/README.md
@@ -0,0 +1,3 @@
+# Module contact
+
+This module provides a simple screen to display contact information.
\ No newline at end of file
diff --git a/spezi-module/contact/build.gradle.kts b/spezi-module/contact/build.gradle.kts
new file mode 100644
index 000000000..a91deadbd
--- /dev/null
+++ b/spezi-module/contact/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+ alias(libs.plugins.spezikt.android.library.compose)
+}
+
+android {
+ namespace = "edu.stanford.spezikt.spezi_module.contact"
+
+ packagingOptions {
+ exclude("META-INF/**.md")
+ }
+}
+
+dependencies {
+ androidTestImplementation(libs.bundles.mockk.androidTestImplementation)
+ testImplementation(libs.truth)
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/androidTest/AndroidManifest.xml b/spezi-module/contact/src/androidTest/AndroidManifest.xml
new file mode 100644
index 000000000..df6897f26
--- /dev/null
+++ b/spezi-module/contact/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/ContactFactory.kt b/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/ContactFactory.kt
new file mode 100644
index 000000000..29087a461
--- /dev/null
+++ b/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/ContactFactory.kt
@@ -0,0 +1,21 @@
+package edu.stanford.spezikt.spezi_module.contact
+
+import androidx.compose.ui.graphics.vector.ImageVector
+import edu.stanford.spezikt.spezi_module.contact.model.Contact
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOption
+import java.util.UUID
+
+object ContactFactory {
+ fun create(
+ id: UUID = UUID.randomUUID(),
+ icon: ImageVector? = null,
+ name: String = "",
+ title: String = "",
+ description: String = "",
+ organization: String = "",
+ address: String = "",
+ options: List = emptyList()
+ ): Contact {
+ return Contact(id, icon, name, title, description, organization, address, options)
+ }
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/ContactScreenTest.kt b/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/ContactScreenTest.kt
new file mode 100644
index 000000000..39e9d2c8a
--- /dev/null
+++ b/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/ContactScreenTest.kt
@@ -0,0 +1,104 @@
+package edu.stanford.spezikt.spezi_module.contact
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Call
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOption
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOptionType
+import edu.stanford.spezikt.spezi_module.contact.repository.ContactRepository
+import io.mockk.every
+import io.mockk.mockk
+import org.junit.Rule
+import org.junit.Test
+import java.util.UUID
+
+class ContactScreenTest {
+
+ private val mockContactRepository: ContactRepository = mockk()
+
+ @get:Rule
+ val composeTestRule = createAndroidComposeRule()
+
+ @Test
+ fun contactView_displaysContactName() {
+ val contact = ContactFactory.create(name = "John Doe")
+ every { mockContactRepository.getContact() } returns contact
+
+ composeTestRule.setContent {
+ ContactScreen(ContactViewModel(mockContactRepository))
+ }
+
+ composeTestRule.onNodeWithText(contact.name).assertExists()
+ }
+
+
+ @Test
+ fun contactView_displaysContactOptions() {
+ val contact = ContactFactory.create(
+ options = listOf(
+ ContactOption(
+ UUID.randomUUID(),
+ "Call",
+ "+49 123 456 789",
+ Icons.Default.Call,
+ ContactOptionType.CALL
+ ),
+ ContactOption(
+ UUID.randomUUID(),
+ "Email",
+ "",
+ Icons.Default.Email,
+ ContactOptionType.EMAIL
+ ),
+ ContactOption(
+ UUID.randomUUID(),
+ "Website",
+ "https://www.google.com",
+ Icons.Default.Info,
+ ContactOptionType.WEBSITE
+ )
+ )
+ )
+ every { mockContactRepository.getContact() } returns contact
+
+ composeTestRule.setContent {
+ ContactScreen(
+ ContactViewModel(mockContactRepository)
+ )
+ }
+
+ mockContactRepository.getContact().options.forEach { option ->
+ composeTestRule.onNodeWithText(option.name).assertExists()
+ }
+ }
+
+ @Test
+ fun contactView_displaysContactTitle() {
+ val contact = ContactFactory.create(title = "CEO")
+ every { mockContactRepository.getContact() } returns contact
+
+ composeTestRule.setContent {
+ ContactScreen(ContactViewModel(mockContactRepository))
+ }
+
+ composeTestRule.onNodeWithText(mockContactRepository.getContact().title)
+ .assertExists()
+ }
+
+ @Test
+ fun contactView_displaysContactDescription() {
+ val contact =
+ ContactFactory.create(description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
+ every { mockContactRepository.getContact() } returns contact
+
+ composeTestRule.setContent {
+ ContactScreen(ContactViewModel(mockContactRepository))
+ }
+
+ composeTestRule.onNodeWithText(mockContactRepository.getContact().description)
+ .assertExists()
+ }
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/TestActivity.kt b/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/TestActivity.kt
new file mode 100644
index 000000000..55b673efb
--- /dev/null
+++ b/spezi-module/contact/src/androidTest/java/edu/stanford/spezikt/spezi_module/contact/TestActivity.kt
@@ -0,0 +1,6 @@
+package edu.stanford.spezikt.spezi_module.contact
+
+import androidx.activity.ComponentActivity
+
+class TestActivity : ComponentActivity() {
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/AndroidManifest.xml b/spezi-module/contact/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/spezi-module/contact/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/ContactScreen.kt b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/ContactScreen.kt
new file mode 100644
index 000000000..96e3ae659
--- /dev/null
+++ b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/ContactScreen.kt
@@ -0,0 +1,150 @@
+package edu.stanford.spezikt.spezi_module.contact
+
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountBox
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import edu.stanford.spezikt.core.design.theme.IconSize
+import edu.stanford.spezikt.core.design.theme.LargeSpacing
+import edu.stanford.spezikt.core.design.theme.MediumSpacing
+import edu.stanford.spezikt.core.design.theme.SmallSpacing
+import edu.stanford.spezikt.core.design.theme.SpeziKtTheme
+import edu.stanford.spezikt.core.design.theme.SpeziTypography
+import edu.stanford.spezikt.spezi_module.contact.component.ContactOptionCard
+import edu.stanford.spezikt.spezi_module.contact.component.NavigationCard
+import edu.stanford.spezikt.spezi_module.contact.model.Contact
+import edu.stanford.spezikt.spezi_module.contact.repository.DefaultContactRepository
+
+/**
+ * ContactView composable function to display contact information.
+ *
+ * @param viewModel The ViewModel associated with the screen.
+ *
+ * @sample edu.stanford.spezikt.spezi_module.contact.ContactScreenPreview
+ *
+ * @see Contact
+ * @see ContactOptionCard
+ * @see NavigationCard
+ * @see edu.stanford.spezikt.spezi_module.contact.repository.ContactRepository
+ */
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun ContactScreen(viewModel: ContactViewModel) {
+ val contact by viewModel.contact.collectAsState()
+
+ Column {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(MediumSpacing),
+ ) {
+ Surface {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(MediumSpacing),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(MediumSpacing))
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(MediumSpacing),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(SmallSpacing),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ contact?.icon ?: Icons.Default.AccountBox,
+ contentDescription = "Profile Picture",
+ modifier = Modifier.size(IconSize)
+ )
+ Column {
+ contact?.let {
+ Text(
+ text = it.name,
+ style = SpeziTypography.titleLarge
+ )
+ }
+ contact?.let {
+ Text(
+ text = it.title,
+ style = SpeziTypography.titleSmall
+ )
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(SmallSpacing))
+ contact?.let {
+ Text(
+ text = it.description,
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ Spacer(modifier = Modifier.height(LargeSpacing))
+ FlowRow(
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ val context = LocalContext.current
+ contact?.options?.forEach { option ->
+ ContactOptionCard(option = option) {
+ viewModel.handleAction(it, context)
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(LargeSpacing))
+ val context = LocalContext.current
+ contact?.let { contact ->
+ NavigationCard(address = contact.address) {
+ viewModel.handleAction(it, context)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ContactScreenPreview(@PreviewParameter(ContactViewModelPreviewParameterProvider::class) viewModel: ContactViewModel) {
+ SpeziKtTheme {
+ ContactScreen(viewModel)
+ }
+}
+
+class ContactViewModelPreviewParameterProvider : PreviewParameterProvider {
+ override val values: Sequence = sequenceOf(
+ ContactViewModel(DefaultContactRepository()),
+ )
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/ContactViewModel.kt b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/ContactViewModel.kt
new file mode 100644
index 000000000..c9bfb4669
--- /dev/null
+++ b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/ContactViewModel.kt
@@ -0,0 +1,59 @@
+package edu.stanford.spezikt.spezi_module.contact
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.lifecycle.ViewModel
+import edu.stanford.spezikt.spezi_module.contact.model.Contact
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOptionType
+import edu.stanford.spezikt.spezi_module.contact.repository.ContactRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class ContactViewModel(private val repository: ContactRepository) : ViewModel() {
+ private val _contact = MutableStateFlow(null)
+ val contact: StateFlow = _contact
+
+ init {
+ fetchContact()
+ }
+
+ private fun fetchContact() {
+ _contact.value = repository.getContact()
+ }
+
+ fun handleAction(action: OnAction, context: Context) {
+ when (action) {
+ is OnAction.CardClicked -> {
+ when (action.option.optionType) {
+ ContactOptionType.EMAIL -> {
+ val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse("mailto:${action.option.value}")
+ }
+ context.startActivity(emailIntent)
+ }
+
+ ContactOptionType.CALL -> {
+ val dialIntent = Intent(Intent.ACTION_DIAL).apply {
+ data = Uri.parse("tel:${action.option.value}")
+ }
+ context.startActivity(dialIntent)
+ }
+
+ ContactOptionType.WEBSITE -> {
+ val browserIntent =
+ Intent(Intent.ACTION_VIEW, Uri.parse(action.option.value))
+ context.startActivity(browserIntent)
+ }
+ }
+ }
+
+ is OnAction.NavigateTo -> {
+ val gmmIntentUri = Uri.parse("geo:0,0?q=${action.address}")
+ val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri)
+ mapIntent.setPackage("com.google.android.apps.maps")
+ context.startActivity(mapIntent)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/OnAction.kt b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/OnAction.kt
new file mode 100644
index 000000000..99272b866
--- /dev/null
+++ b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/OnAction.kt
@@ -0,0 +1,8 @@
+package edu.stanford.spezikt.spezi_module.contact
+
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOption
+
+sealed interface OnAction {
+ data class CardClicked(val option: ContactOption) : OnAction
+ data class NavigateTo(val address: String) : OnAction
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/component/ContactOptionCard.kt b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/component/ContactOptionCard.kt
new file mode 100644
index 000000000..2f1ccb0f8
--- /dev/null
+++ b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/component/ContactOptionCard.kt
@@ -0,0 +1,76 @@
+package edu.stanford.spezikt.spezi_module.contact.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import edu.stanford.spezikt.core.design.theme.SmallSpacing
+import edu.stanford.spezikt.core.design.theme.SpeziKtTheme
+import edu.stanford.spezikt.spezi_module.contact.OnAction
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOption
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOptionType
+import java.util.UUID
+
+/**
+ * A card that displays a contact option.
+ * @param option The contact option to display.
+ * @sample edu.stanford.spezikt.spezi_module.contact.component.ContactOptionCardPreview
+ * @see ContactOption
+ * @see ContactOptionType
+ * @see edu.stanford.spezikt.spezi_module.contact.ContactScreen
+ */
+@Composable
+fun ContactOptionCard(option: ContactOption, publisher: (OnAction) -> Unit) {
+ Card(
+ modifier = Modifier
+ .wrapContentSize(Alignment.Center)
+ .width(90.dp)
+ .clickable {
+ publisher(OnAction.CardClicked(option = option))
+ }
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .padding(SmallSpacing)
+ .fillMaxWidth()
+ ) {
+ Icon(
+ option.icon ?: Icons.Default.Email,
+ contentDescription = option.name
+ )
+ Text(
+ text = option.name,
+ )
+ }
+ }
+}
+
+@Composable
+@Preview
+fun ContactOptionCardPreview() {
+ SpeziKtTheme {
+ ContactOptionCard(
+ option = ContactOption(
+ id = UUID.randomUUID(),
+ name = "Email",
+ value = "test@test.de",
+ icon = Icons.Default.Email,
+ optionType = ContactOptionType.EMAIL
+ ),
+ publisher = { action -> println(action) }
+ )
+ }
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/component/NavigationCard.kt b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/component/NavigationCard.kt
new file mode 100644
index 000000000..0f75894a7
--- /dev/null
+++ b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/component/NavigationCard.kt
@@ -0,0 +1,74 @@
+package edu.stanford.spezikt.spezi_module.contact.component
+
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import edu.stanford.spezikt.core.design.theme.MediumSpacing
+import edu.stanford.spezikt.core.design.theme.SpeziKtTheme
+import edu.stanford.spezikt.spezi_module.contact.OnAction
+
+/**
+ * A card that displays an address and allows the user to navigate to it.
+ * @param address The address to display and navigate to.
+ * @sample edu.stanford.spezikt.spezi_module.contact.component.NavigationCardPreview
+ * @see edu.stanford.spezikt.spezi_module.contact.ContactScreen
+ */
+@Composable
+fun NavigationCard(address: String, publisher: (OnAction) -> Unit) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(MediumSpacing)
+ .height(IntrinsicSize.Min),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Place,
+ contentDescription = "Address",
+ )
+ Text(
+ text = address,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.weight(1f)
+ )
+ IconButton(
+ onClick = {
+ publisher(OnAction.NavigateTo(address = address))
+ },
+ modifier = Modifier.align(Alignment.Top)
+ ) {
+ Icon(
+ Icons.AutoMirrored.Filled.Send,
+ contentDescription = "Navigate to"
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@Preview
+fun NavigationCardPreview() {
+ SpeziKtTheme {
+ NavigationCard("1234 Main Street, 12345 City",
+ publisher = { action -> println(action) })
+ }
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/model/Contact.kt b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/model/Contact.kt
new file mode 100644
index 000000000..f8345f835
--- /dev/null
+++ b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/model/Contact.kt
@@ -0,0 +1,59 @@
+package edu.stanford.spezikt.spezi_module.contact.model
+
+import androidx.compose.ui.graphics.vector.ImageVector
+import java.util.UUID
+
+/**
+ * Contact data class used to represent a contact.
+ *
+ * @param id the unique identifier of the contact
+ * @param icon the icon of the contact
+ * @param name the name of the contact
+ * @param title the title of the contact
+ * @param description the description of the contact
+ * @param organization the organization of the contact
+ * @param address the address of the contact
+ * @param options the list of contact options
+ *
+ * @see ContactOption
+ * @see ContactOptionType
+ *
+ */
+data class Contact(
+ val id: UUID,
+ val icon: ImageVector?,
+ var name: String,
+ val title: String,
+ val description: String,
+ val organization: String,
+ val address: String,
+ val options: List
+)
+
+/**
+ * ContactOption data class used to represent a contact option.
+ *
+ * @param id the unique identifier of the contact option
+ * @param name the name of the contact option
+ * @param value the value of the contact option
+ * @param icon the icon of the contact option
+ * @param optionType the type of the contact option
+ *
+ * @see ContactOptionType
+ */
+data class ContactOption(
+ val id: UUID,
+ val name: String,
+ val value: String,
+ val icon: ImageVector?,
+ val optionType: ContactOptionType
+)
+
+/**
+ * ContactOptionType enum class used to represent the type of a contact option.
+ *
+ * @see ContactOption
+ */
+enum class ContactOptionType {
+ CALL, EMAIL, WEBSITE
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/repository/ContactRepository.kt b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/repository/ContactRepository.kt
new file mode 100644
index 000000000..2214706be
--- /dev/null
+++ b/spezi-module/contact/src/main/java/edu/stanford/spezikt/spezi_module/contact/repository/ContactRepository.kt
@@ -0,0 +1,57 @@
+package edu.stanford.spezikt.spezi_module.contact.repository
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Call
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Info
+import edu.stanford.spezikt.spezi_module.contact.model.Contact
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOption
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOptionType
+import java.util.UUID
+
+/**
+ * ContactRepository interface used to define the contact repository.
+ */
+interface ContactRepository {
+ fun getContact(): Contact
+}
+
+/**
+ * DefaultContactRepository class used to provide the default contact repository.
+ */
+class DefaultContactRepository : ContactRepository {
+ override fun getContact(): Contact {
+ return Contact(
+ id = UUID.randomUUID(),
+ icon = null,
+ name = "Leland Stanford",
+ title = "CEO",
+ description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ organization = "Stanford University",
+ address = "450 Jane Stanford Way Stanford, CA",
+ options = listOf(
+ ContactOption(
+ UUID.randomUUID(),
+ "Call",
+ "+49 123 456 789",
+ Icons.Default.Call,
+ ContactOptionType.CALL
+ ),
+ ContactOption(
+ UUID.randomUUID(),
+ "Email",
+ "test@gmail.com",
+ Icons.Default.Email,
+ ContactOptionType.EMAIL
+ ),
+ ContactOption(
+ UUID.randomUUID(),
+ "Website",
+ "https://www.google.com",
+ Icons.Default.Info,
+ ContactOptionType.WEBSITE
+ )
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/spezi-module/contact/src/test/java/edu/stanford/spezikt/spezi_module/contact/ContactRepositoryTest.kt b/spezi-module/contact/src/test/java/edu/stanford/spezikt/spezi_module/contact/ContactRepositoryTest.kt
new file mode 100644
index 000000000..f08e5127f
--- /dev/null
+++ b/spezi-module/contact/src/test/java/edu/stanford/spezikt/spezi_module/contact/ContactRepositoryTest.kt
@@ -0,0 +1,35 @@
+package edu.stanford.spezikt.spezi_module.contact
+
+import com.google.common.truth.Truth.assertThat
+import edu.stanford.spezikt.spezi_module.contact.model.ContactOptionType
+import edu.stanford.spezikt.spezi_module.contact.repository.DefaultContactRepository
+import org.junit.Before
+import org.junit.Test
+
+class DefaultContactRepositoryTest {
+
+ private lateinit var repository: DefaultContactRepository
+
+ @Before
+ fun setup() {
+ repository = DefaultContactRepository()
+ }
+
+ @Test
+ fun defaultContactRepository_loadsContact() {
+ val contact = repository.getContact()
+ with(contact) {
+ assertThat(name).isEqualTo("Leland Stanford")
+ assertThat(title).isEqualTo("CEO")
+ assertThat(description).isEqualTo(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+ )
+ assertThat(organization).isEqualTo("Stanford University")
+ assertThat(address).isEqualTo("450 Jane Stanford Way Stanford, CA")
+ assertThat(options.size).isEqualTo(3)
+ assertThat(options[0].name).isEqualTo("Call")
+ assertThat(options[0].value).isEqualTo("+49 123 456 789")
+ assertThat(options[0].optionType).isEqualTo(ContactOptionType.CALL)
+ }
+ }
+}
\ No newline at end of file
diff --git a/spezi-module/onboarding/.gitignore b/spezi-module/onboarding/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/spezi-module/onboarding/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/spezi-module/onboarding/README.md b/spezi-module/onboarding/README.md
new file mode 100644
index 000000000..72e1ef928
--- /dev/null
+++ b/spezi-module/onboarding/README.md
@@ -0,0 +1,3 @@
+# Module onboarding
+
+This module provides a simple onboarding screen.
\ No newline at end of file
diff --git a/spezi-module/onboarding/build.gradle.kts b/spezi-module/onboarding/build.gradle.kts
new file mode 100644
index 000000000..0ae96fa33
--- /dev/null
+++ b/spezi-module/onboarding/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ alias(libs.plugins.spezikt.android.library.compose)
+}
+
+android {
+ namespace = "edu.stanford.spezikt.spezi_module.onboarding"
+}
\ No newline at end of file
diff --git a/spezi-module/onboarding/src/main/AndroidManifest.xml b/spezi-module/onboarding/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..44008a433
--- /dev/null
+++ b/spezi-module/onboarding/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/OnboardingScreen.kt b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/OnboardingScreen.kt
new file mode 100644
index 000000000..c4b6ea828
--- /dev/null
+++ b/spezi-module/onboarding/src/main/java/edu/stanford/spezikt/spezi_module/onboarding/OnboardingScreen.kt
@@ -0,0 +1,20 @@
+package edu.stanford.spezikt.spezi_module.onboarding
+
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import edu.stanford.spezikt.core.design.theme.SpeziKtTheme
+
+@Composable
+fun OnboardingScreen() {
+ Text(text = "Onboarding Screen")
+}
+
+
+@Preview
+@Composable
+fun OnboardingScreenPreview() {
+ SpeziKtTheme {
+ OnboardingScreen()
+ }
+}
\ No newline at end of file