diff --git a/.gitignore b/.gitignore
index 33e4c75..de47acd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ xcuserdata
!src/**/build/
local.properties
.idea
+.kotlin
.DS_Store
captures
.externalNativeBuild
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..8275fe8
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,172 @@
+# Attribution-ShareAlike 4.0 International
+Twilight © Copyright by Delacrix Morgan
+
+Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
+
+### Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
+
+* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors).
+
+* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees).
+
+## Creative Commons Attribution-ShareAlike 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
+
+### Section 1 – Definitions.
+
+a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
+
+b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
+
+c. __BY-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License.
+
+d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
+
+e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
+
+f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
+
+g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
+
+h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
+
+i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
+
+j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License.
+
+k. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
+
+l. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
+
+m. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
+
+### Section 2 – Scope.
+
+a. ___License grant.___
+
+1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
+
+ A. reproduce and Share the Licensed Material, in whole or in part; and
+
+ B. produce, reproduce, and Share Adapted Material.
+
+2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
+
+3. __Term.__ The term of this Public License is specified in Section 6(a).
+
+4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
+
+5. __Downstream recipients.__
+
+ A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
+
+ B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
+
+ C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
+
+6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
+
+b. ___Other rights.___
+
+1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
+
+2. Patent and trademark rights are not licensed under this Public License.
+
+3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
+
+### Section 3 – License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the following conditions.
+
+a. ___Attribution.___
+
+1. If You Share the Licensed Material (including in modified form), You must:
+
+ A. retain the following if it is supplied by the Licensor with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
+
+ B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
+
+ C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
+
+2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
+
+3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
+
+b. ___ShareAlike.___
+
+In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
+
+1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
+
+2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
+
+3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
+
+### Section 4 – Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
+
+a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
+
+b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
+
+c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
+
+### Section 5 – Disclaimer of Warranties and Limitation of Liability.
+
+a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__
+
+b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__
+
+c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
+
+### Section 6 – Term and Termination.
+
+a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
+
+b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
+
+1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
+
+2. upon express reinstatement by the Licensor.
+
+For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
+
+c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
+
+d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
+
+### Section 7 – Other Terms and Conditions.
+
+a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
+
+b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
+
+### Section 8 – Interpretation.
+
+a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
+
+b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
+
+c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
+
+d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
+
+> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the [CC0 Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/legalcode). Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
+>
+> Creative Commons may be contacted at creativecommons.org.
\ No newline at end of file
diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md
new file mode 100644
index 0000000..de12383
--- /dev/null
+++ b/PRIVACY_POLICY.md
@@ -0,0 +1,96 @@
+## Privacy Policy
+
+Delacrix Morgan built the Twilight app as a Free and Open Source app. This SERVICE is provided by
+Delacrix Morgan at no cost and is intended for use as is.
+
+This page is used to inform visitors regarding my policies with the collection, use, and disclosure
+of Personal Information if anyone decided to use my Service.
+
+If you choose to use my Service, then you agree to the collection and use of information in relation
+to this policy. The Personal Information that I collect is used for providing and improving the
+Service. I will not use or share your information with anyone except as described in this Privacy
+Policy.
+
+The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which
+is accessible at Twilight unless otherwise defined in this Privacy Policy.
+
+**Information Collection and Use**
+
+For a better experience, while using our Service, I may require you to provide us with certain
+personally identifiable information. The information that I request will be retained on your device
+and is not collected by me in any way.
+
+The app does use third party services that may collect information used to identify you.
+
+Link to privacy policy of third party service providers used by the app
+
+* [Google Play Services](https://www.google.com/policies/privacy/)
+* [Firebase Analytics](https://firebase.google.com/policies/analytics)
+
+**Log Data**
+
+I want to inform you that whenever you use my Service, in a case of an error in the app I collect
+data and information (through third party products) on your phone called Log Data. This Log Data may
+include information such as your device Internet Protocol (“IP”) address, device name, operating
+system version, the configuration of the app when utilizing my Service, the time and date of your
+use of the Service, and other statistics.
+
+**Cookies**
+
+Cookies are files with a small amount of data that are commonly used as anonymous unique
+identifiers. These are sent to your browser from the websites that you visit and are stored on your
+device's internal memory.
+
+This Service does not use these “cookies” explicitly. However, the app may use third party code and
+libraries that use “cookies” to collect information and improve their services. You have the option
+to either accept or refuse these cookies and know when a cookie is being sent to your device. If you
+choose to refuse our cookies, you may not be able to use some portions of this Service.
+
+**Service Providers**
+
+I may employ third-party companies and individuals due to the following reasons:
+
+* To facilitate our Service;
+* To provide the Service on our behalf;
+* To perform Service-related services; or
+* To assist us in analyzing how our Service is used.
+
+I want to inform users of this Service that these third parties have access to your Personal
+Information. The reason is to perform the tasks assigned to them on our behalf. However, they are
+obligated not to disclose or use the information for any other purpose.
+
+**Security**
+
+I value your trust in providing us your Personal Information, thus we are striving to use
+commercially acceptable means of protecting it. But remember that no method of transmission over the
+internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its
+absolute security.
+
+**Links to Other Sites**
+
+This Service may contain links to other sites. If you click on a third-party link, you will be
+directed to that site. Note that these external sites are not operated by me. Therefore, I strongly
+advise you to review the Privacy Policy of these websites. I have no control over and assume no
+responsibility for the content, privacy policies, or practices of any third-party sites or services.
+
+**Children’s Privacy**
+
+These Services do not address anyone under the age of 13\. I do not knowingly collect personally
+identifiable information from children under 13\. In the case I discover that a child under 13 has
+provided me with personal information, I immediately delete this from our servers. If you are a
+parent or guardian and you are aware that your child has provided us with personal information,
+please contact me so that I will be able to do necessary actions.
+
+**Changes to This Privacy Policy**
+
+I may update our Privacy Policy from time to time. Thus, you are advised to review this page
+periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on
+this page. These changes are effective immediately after they are posted on this page.
+
+**Contact Us**
+
+If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me.
+
+This privacy policy page was created
+at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated
+by [App Privacy Policy Generator](https://app-privacy-policy-generator.firebaseapp.com/)
diff --git a/README.md b/README.md
index 3036737..bb9d5a4 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,72 @@
-This is a Kotlin Multiplatform project targeting Android, iOS, Web, Desktop.
+# Twilight - Timezone Tracker
-* `/composeApp` is for code that will be shared across your Compose Multiplatform applications.
- It contains several subfolders:
- - `commonMain` is for code that’s common for all targets.
- - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name.
- For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app,
- `iosMain` would be the right folder for such calls.
+## Overview
-* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform,
- you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
+Personalise your timezones with the ability to time travel! 🕓
+## Description
-Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html),
-[Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform/#compose-multiplatform),
-[Kotlin/Wasm](https://kotl.in/wasm/)…
+Twilight is a timezone tracking app that makes convert time fun like a fidget spinner. You can easily add fun little nicknames to your locations and travel ahead in time to settle on your next
+gaming session!
-**Note:** Compose/Web is Experimental and may be changed at any time. Use it only for evaluation purposes.
-We would appreciate your feedback on Compose/Web and Kotlin/Wasm in the public Slack channel [#compose-web](https://slack-chats.kotlinlang.org/c/compose-web).
-If you face any issues, please report them on [GitHub](https://github.com/JetBrains/compose-multiplatform/issues).
+**a) Customisable Names**
-You can open the web application by running the `:composeApp:wasmJsBrowserDevelopmentRun` Gradle task.
\ No newline at end of file
+💬 Do you have friends and family that lives in different countries with annoying timezones and you always have trouble knowing whether it is night time at their local time or not?
+
+Fret not! Now, you can have their timezones all in one place with fun little names (optional).
+
+You can just easily add them as:
+
+- “Little Simba 😸”
+- “Eggs and Ham 🥚”
+- “My Little Princess
+
+💬 Do you work remotely with team members from all around the world and always a pain to setup a meeting time based on everyone's local time?
+
+Sure, you can just Google their local time. But, think about the amount of letters you have to type everytime!
+
+You can just easily add them as:
+
+- "London's Office 🇬🇧"
+- "Munich Drinking Buddies 🇩🇪"
+- “Japan Animation Team 🇯️🇵️”
+
+**b) Time Travel with the wibbly-wobbly, timey-wimey Spinner**
+
+💬 Yes, you've read that right! You can now travel ahead of time and see what time is it going to be. (Mind blowing, I know).
+
+The measuring tape looking slider, give that a spin and it will transport you through time. You can instantly see the local time of your saved timezones and how they fare up. Then, you can finally make a call with that one of friend of yours without accidentally waking them up.
+
+**c) Dark Mode**
+
+Although the name of the app is Twilight, but we do support both Light and Dark mode. All spectrum of light deserves the same amount of love and attention.
+
+If you made it this far in the description, thank you for your support! Timezones is always been my arch enemy, next to math and physics. Would love to hear about what you think about the app!
+
+In the meantime, go time travel!
+
+[Google Play Store - Twilight Timezone Tracker](https://play.google.com/store/apps/details?id=com.delacrixmorgan.twilight.android)
+
+## Nerdy Stats
+
+- Kotlin Multiplatform
+- Date - Kotlinx Datetime
+- Dependency Injection - Koin
+- Navigation - Compose Navigation
+- Local Storage - Data Store, SQL Delight
+- Logging - Kermit
+
+## Screenshots
+
+![Today](/screenshots/1_today.png?raw=true "Today")
+
+![Form](/screenshots/2_form.png?raw=true "Form")
+
+## License
+
+```
+Twilight © by Delacrix Morgan
+Licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
+```
+
+[Creative Commons CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/legalcode)
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 52cf9fa..dd2e556 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,5 +4,6 @@ plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.jetbrainsCompose) apply false
+ alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}
\ No newline at end of file
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index c4b6c82..ffeb354 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -1,41 +1,38 @@
-import org.jetbrains.compose.desktop.application.dsl.TargetFormat
-import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
-import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
+ alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsCompose)
+ alias(libs.plugins.composeCompiler)
+ alias(libs.plugins.sqlDelight)
}
-kotlin {
- @OptIn(ExperimentalWasmDsl::class)
- wasmJs {
- moduleName = "composeApp"
- browser {
- commonWebpackConfig {
- outputFileName = "composeApp.js"
- devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
- static = (static ?: mutableListOf()).apply {
- // Serve sources to debug inside browser
- add(project.projectDir.path)
- }
- }
- }
+repositories {
+ google()
+ mavenCentral()
+}
+
+sqldelight {
+ databases {
+ create("TwilightDatabase") {
+ packageName.set("com.delacrixmorgan.twilight")
}
- binaries.executable()
}
-
+}
+
+kotlin {
androidTarget {
compilations.all {
- kotlinOptions {
- jvmTarget = "11"
+ compileTaskProvider.configure {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_20)
+ }
}
}
}
-
- jvm("desktop")
-
+
listOf(
iosX64(),
iosArm64(),
@@ -46,30 +43,50 @@ kotlin {
isStatic = true
}
}
-
+
sourceSets {
- val desktopMain by getting
-
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
- implementation(libs.androidx.activity.compose)
+ implementation(libs.koin.android)
+ implementation(libs.sqldelight.android)
+ }
+ iosMain.dependencies {
+ implementation(libs.sqldelight.native)
}
commonMain.dependencies {
+ // Common
+ implementation(libs.kermit)
+ implementation(libs.kotlinx.serialization.json)
+ api(libs.kotlinx.datetime)
+ implementation(project.dependencies.platform(libs.koin.bom))
+ implementation(libs.koin.core)
+ implementation(libs.koin.composeVM)
+ implementation(libs.sqldelight.coroutines.extensions)
+
+ // Compose
+ implementation(project.dependencies.platform(libs.compose.bom))
+ implementation(libs.compose.icons.extended)
implementation(compose.runtime)
implementation(compose.foundation)
- implementation(compose.material)
+ implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
+
+ // AndroidX
+ implementation(libs.androidx.lifecycle.viewmodel)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.datastore.preference)
}
- desktopMain.dependencies {
- implementation(compose.desktop.currentOs)
+ commonTest.dependencies {
+ implementation(libs.kotlin.test)
+ implementation(libs.koin.test)
}
}
}
android {
- namespace = "com.delacrixmorgan.twilight"
+ namespace = "com.delacrixmorgan.twilight.android"
compileSdk = libs.versions.android.compileSdk.get().toInt()
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
@@ -77,11 +94,14 @@ android {
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
defaultConfig {
- applicationId = "com.delacrixmorgan.twilight"
+ applicationId = "com.delacrixmorgan.twilight.android"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
- versionCode = 1
- versionName = "1.0"
+ versionCode = 4
+ versionName = "2024.01"
+
+ buildConfigField("int", "VERSION_CODE", versionCode.toString())
+ buildConfigField("String", "VERSION_NAME", "\"${versionName}\"")
}
packaging {
resources {
@@ -90,30 +110,17 @@ android {
}
buildTypes {
getByName("release") {
- isMinifyEnabled = false
+ isMinifyEnabled = true
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_11
- targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_20
+ targetCompatibility = JavaVersion.VERSION_20
+ }
+ buildFeatures {
+ buildConfig = true
}
dependencies {
debugImplementation(libs.compose.ui.tooling)
}
-}
-
-compose.desktop {
- application {
- mainClass = "MainKt"
-
- nativeDistributions {
- targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
- packageName = "com.delacrixmorgan.twilight"
- packageVersion = "1.0.0"
- }
- }
-}
-
-compose.experimental {
- web.application {}
}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index b87d944..7670150 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -2,22 +2,21 @@
+ android:exported="true">
-
-
diff --git a/composeApp/src/androidMain/kotlin/Platform.android.kt b/composeApp/src/androidMain/kotlin/Platform.android.kt
deleted file mode 100644
index 4f3ea05..0000000
--- a/composeApp/src/androidMain/kotlin/Platform.android.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-import android.os.Build
-
-class AndroidPlatform : Platform {
- override val name: String = "Android ${Build.VERSION.SDK_INT}"
-}
-
-actual fun getPlatform(): Platform = AndroidPlatform()
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/MainActivity.kt
deleted file mode 100644
index fd8fe23..0000000
--- a/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/MainActivity.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.delacrixmorgan.twilight
-
-import App
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.tooling.preview.Preview
-
-class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- App()
- }
- }
-}
-
-@Preview
-@Composable
-fun AppAndroidPreview() {
- App()
-}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/android/App.kt b/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/android/App.kt
new file mode 100644
index 0000000..0935611
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/android/App.kt
@@ -0,0 +1,16 @@
+package com.delacrixmorgan.twilight.android
+
+import android.app.Application
+import di.initKoin
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ initKoin {
+ androidContext(this@App)
+ androidLogger()
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/android/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/android/MainActivity.kt
new file mode 100644
index 0000000..47e9002
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/com/delacrixmorgan/twilight/android/MainActivity.kt
@@ -0,0 +1,77 @@
+package com.delacrixmorgan.twilight.android
+
+import App
+import android.content.res.Configuration
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.displayCutout
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.view.WindowCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import data.preferences.model.ThemePreference
+import data.preferences.PreferencesRepository
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import ui.theme.AppTheme
+
+class MainActivity : ComponentActivity(), KoinComponent {
+ private val preferences: PreferencesRepository by inject()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ val theme = remember { mutableStateOf(ThemePreference.Default) }
+ AppTheme(theme.value) {
+ Scaffold {
+ val insetModifier = Modifier
+ .windowInsetsPadding(WindowInsets.displayCutout)
+ .consumeWindowInsets(it)
+ App(insetModifier)
+ }
+ }
+ val view = LocalView.current
+ SideEffect {
+ val window = (view.context as ComponentActivity).window
+ val isDarkTheme = when (theme.value) {
+ ThemePreference.System -> isSystemInDarkTheme()
+ ThemePreference.Light -> false
+ ThemePreference.Dark -> true
+ }
+ WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !isDarkTheme
+ }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ LaunchedEffect(LocalLifecycleOwner.current) {
+ lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
+ preferences.getTheme().collect { theme.value = it }
+ }
+ }
+ }
+ }
+
+ private fun isSystemInDarkTheme(): Boolean {
+ val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ return currentNightMode == Configuration.UI_MODE_NIGHT_YES
+ }
+}
+
+@Preview
+@Composable
+fun AppAndroidPreview() {
+ App()
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/kotlin/expect.android.kt b/composeApp/src/androidMain/kotlin/expect.android.kt
new file mode 100644
index 0000000..c488768
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/expect.android.kt
@@ -0,0 +1,34 @@
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import app.cash.sqldelight.driver.android.AndroidSqliteDriver
+import com.delacrixmorgan.twilight.TwilightDatabase
+import data.utils.LocalDataStore
+import di.TwilightDatabaseWrapper
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+import java.util.UUID
+import com.delacrixmorgan.twilight.android.BuildConfig
+
+actual fun platformModule() = module {
+ single(named(LocalDataStore.Preferences.name)) { dataStore(get(), LocalDataStore.Preferences.path()) }
+ single(named(LocalDataStore.CreateNewLocation.name)) { dataStore(get(), LocalDataStore.CreateNewLocation.path()) }
+ single {
+ val driver = AndroidSqliteDriver(TwilightDatabase.Schema, get(), "twilight.db")
+ TwilightDatabaseWrapper(TwilightDatabase(driver))
+ }
+}
+
+fun dataStore(context: Context, path: String): DataStore = createDataStore(
+ producePath = { context.filesDir.resolve(path).absolutePath }
+)
+
+actual fun randomUUID(): String = UUID.randomUUID().toString()
+
+actual fun getVersionCode(): String {
+ return BuildConfig.VERSION_CODE.toString()
+}
+
+actual fun getVersionName(): String {
+ return BuildConfig.VERSION_NAME
+}
\ No newline at end of file
diff --git a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9..0000000
--- a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
index eca70cf..a83cd23 100644
--- a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,6 @@
-
-
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
index eca70cf..a83cd23 100644
--- a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,6 @@
-
-
+
+
+
\ No newline at end of file
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index a571e60..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..592613d
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..c0815fb
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..647554d
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.webp
new file mode 100644
index 0000000..ba12876
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index 61da551..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..720c924
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index c41dd28..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4babcd5
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..d5eabf6
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..4f639be
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.webp
new file mode 100644
index 0000000..e2be840
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index db5080a..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..0bcc21d
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 6dba46d..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..79c9cfd
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..b46b4ca
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..cb70948
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.webp
new file mode 100644
index 0000000..7a882bd
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index da31a87..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..6559d68
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 15ac681..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..dd47e58
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..a8ce3eb
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..187b31d
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.webp
new file mode 100644
index 0000000..4beee63
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index b216f2d..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..e88f858
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index f25a419..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..bc0c5d2
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..4273e18
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..e2dc667
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp
new file mode 100644
index 0000000..a37364e
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index e96783c..0000000
Binary files a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..27b4e08
Binary files /dev/null and b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
deleted file mode 100644
index d7bf795..0000000
--- a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_logo_foreground.webp b/composeApp/src/commonMain/composeResources/drawable/ic_logo_foreground.webp
new file mode 100644
index 0000000..f285f0e
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/ic_logo_foreground.webp differ
diff --git a/composeApp/src/commonMain/composeResources/font/lato_bold.ttf b/composeApp/src/commonMain/composeResources/font/lato_bold.ttf
new file mode 100644
index 0000000..016068b
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/lato_bold.ttf differ
diff --git a/composeApp/src/commonMain/composeResources/font/lato_regular.ttf b/composeApp/src/commonMain/composeResources/font/lato_regular.ttf
new file mode 100644
index 0000000..bb2e887
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/lato_regular.ttf differ
diff --git a/composeApp/src/commonMain/composeResources/font/league_spartan_regular.ttf b/composeApp/src/commonMain/composeResources/font/league_spartan_regular.ttf
new file mode 100644
index 0000000..c36ea8b
Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/league_spartan_regular.ttf differ
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
new file mode 100644
index 0000000..1e00d90
--- /dev/null
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Twilight
+
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/App.kt b/composeApp/src/commonMain/kotlin/App.kt
index 660abc4..55609cc 100644
--- a/composeApp/src/commonMain/kotlin/App.kt
+++ b/composeApp/src/commonMain/kotlin/App.kt
@@ -1,40 +1,7 @@
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material.Button
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import org.jetbrains.compose.resources.ExperimentalResourceApi
-import org.jetbrains.compose.resources.painterResource
-import org.jetbrains.compose.ui.tooling.preview.Preview
-import twilight.composeapp.generated.resources.Res
-import twilight.composeapp.generated.resources.compose_multiplatform
-@OptIn(ExperimentalResourceApi::class)
@Composable
-@Preview
-fun App() {
- MaterialTheme {
- var showContent by remember { mutableStateOf(false) }
- Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
- Button(onClick = { showContent = !showContent }) {
- Text("Click me!")
- }
- AnimatedVisibility(showContent) {
- val greeting = remember { Greeting().greet() }
- Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
- Image(painterResource(Res.drawable.compose_multiplatform), null)
- Text("Compose: $greeting")
- }
- }
- }
- }
+fun App(modifier: Modifier = Modifier) {
+ TwilightApp()
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/Greeting.kt b/composeApp/src/commonMain/kotlin/Greeting.kt
deleted file mode 100644
index 887d835..0000000
--- a/composeApp/src/commonMain/kotlin/Greeting.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-class Greeting {
- private val platform = getPlatform()
-
- fun greet(): String {
- return "Hello, ${platform.name}!"
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/Platform.kt b/composeApp/src/commonMain/kotlin/Platform.kt
deleted file mode 100644
index 87ca3ff..0000000
--- a/composeApp/src/commonMain/kotlin/Platform.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-interface Platform {
- val name: String
-}
-
-expect fun getPlatform(): Platform
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/TwilightApp.kt b/composeApp/src/commonMain/kotlin/TwilightApp.kt
new file mode 100644
index 0000000..d6c013f
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/TwilightApp.kt
@@ -0,0 +1,7 @@
+import androidx.compose.runtime.Composable
+import nav.TwilightNavHost
+
+@Composable
+fun TwilightApp() {
+ TwilightNavHost()
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/KairosRepository.kt b/composeApp/src/commonMain/kotlin/data/kairos/KairosRepository.kt
new file mode 100644
index 0000000..be97ec1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/KairosRepository.kt
@@ -0,0 +1,62 @@
+package data.kairos
+
+import data.kairos.mapper.AmericaZoneIdToZoneMapper
+import data.kairos.mapper.AsiaZoneIdToZoneMapper
+import data.kairos.mapper.AustraliaZoneIdToZoneMapper
+import data.kairos.mapper.EuropeZoneIdToZoneMapper
+import data.kairos.mapper.PacificZoneIdToZoneMapper
+import data.kairos.model.Region
+import data.kairos.model.Zone
+import kotlinx.datetime.TimeZone
+import org.koin.core.component.KoinComponent
+
+/**
+ * Kairos, ancient Greek word meaning 'the right or critical moment'
+ * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ */
+class KairosRepository : KoinComponent {
+ val zones: List
+ private val americaZoneIdToZoneMapper by lazy { AmericaZoneIdToZoneMapper() }
+ private val asiaZoneIdToZoneMapper by lazy { AsiaZoneIdToZoneMapper() }
+ private val australiaZoneIdToZoneMapper by lazy { AustraliaZoneIdToZoneMapper() }
+ private val europeZoneIdToZoneMapper by lazy { EuropeZoneIdToZoneMapper() }
+ private val pacificZoneIdToZoneMapper by lazy { PacificZoneIdToZoneMapper() }
+
+ init {
+ val availableZones: Set = TimeZone.availableZoneIds
+ zones = Region.entries.toTypedArray().flatMap { region ->
+ availableZones.filter { it.contains("$region/") }
+ .transformZoneIds(region)
+ }
+ }
+
+ private fun List.transformZoneIds(
+ region: Region
+ ): List = when (region) {
+ Region.Africa -> genericZoneIdToTimezoneMapper(region)
+ Region.America -> americaZoneIdToZoneMapper(this)
+ Region.Antarctica -> genericZoneIdToTimezoneMapper(region)
+ Region.Arctic -> genericZoneIdToTimezoneMapper(region)
+ Region.Asia -> asiaZoneIdToZoneMapper(this)
+ Region.Atlantic -> genericZoneIdToTimezoneMapper(region)
+ Region.Australia -> australiaZoneIdToZoneMapper(this)
+ Region.Brazil -> genericZoneIdToTimezoneMapper(region)
+ Region.Canada -> genericZoneIdToTimezoneMapper(region)
+ Region.Chile -> genericZoneIdToTimezoneMapper(region)
+ Region.Europe -> europeZoneIdToZoneMapper(this)
+ Region.Indian -> genericZoneIdToTimezoneMapper(region)
+ Region.Mexico -> genericZoneIdToTimezoneMapper(region)
+ Region.Pacific -> pacificZoneIdToZoneMapper(this)
+ Region.US -> genericZoneIdToTimezoneMapper(region)
+ }
+
+ private fun List.genericZoneIdToTimezoneMapper(
+ region: Region
+ ): List = map {
+ Zone(zoneIdString = it, region = region)
+ }
+
+ fun search(zoneId: String?): Zone? {
+ return zones.firstOrNull { it.zoneIdString == zoneId }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/mapper/AmericaZoneIdToZoneMapper.kt b/composeApp/src/commonMain/kotlin/data/kairos/mapper/AmericaZoneIdToZoneMapper.kt
new file mode 100644
index 0000000..c6d94fd
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/mapper/AmericaZoneIdToZoneMapper.kt
@@ -0,0 +1,23 @@
+package data.kairos.mapper
+
+import data.kairos.model.Region
+import data.kairos.model.Zone
+import data.utils.Mapper
+
+class AmericaZoneIdToZoneMapper : Mapper, List> {
+ companion object {
+ private val region = Region.America
+ }
+
+ override fun invoke(input: List): List = input.map { zoneIdString ->
+ when (zoneIdString) {
+ "$region/New_York" -> Zone(
+ zoneIdString, region,
+ country = listOf("United States", "US", "\uD83C\uDDFA\uD83C\uDDF8"),
+ states = listOf("Ohio"),
+ cities = listOf("Cincinnati")
+ )
+ else -> Zone(zoneIdString, region)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/mapper/AsiaZoneIdToZoneMapper.kt b/composeApp/src/commonMain/kotlin/data/kairos/mapper/AsiaZoneIdToZoneMapper.kt
new file mode 100644
index 0000000..6a83ba6
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/mapper/AsiaZoneIdToZoneMapper.kt
@@ -0,0 +1,169 @@
+package data.kairos.mapper
+
+import data.kairos.model.Region
+import data.kairos.model.Zone
+import data.utils.Mapper
+
+class AsiaZoneIdToZoneMapper : Mapper, List> {
+ companion object {
+ private val region = Region.Asia
+ }
+
+ override fun invoke(input: List): List = input.map { zoneIdString ->
+ when (zoneIdString) {
+ // South East Asia
+ "$region/Brunei" -> Zone(
+ zoneIdString, region,
+ country = listOf("Brunei", "BN", "\uD83C\uDDE7\uD83C\uDDF3"),
+ states = listOf("Brunei-Muara", "Belait", "Tutong", "Temburong"),
+ cities = listOf("Bandar Seri Begawan", "Kuala Belait", "Seria", "Tutong", "Bangar")
+ )
+ "$region/Phnom_Penh" -> Zone(
+ zoneIdString, region,
+ country = listOf("Cambodia", "KH", "\uD83C\uDDF0\uD83C\uDDED"),
+ states = listOf("Phnom Penh", "Battambang", "Siem Reap", "Sihanoukville", "Kampong Cham", "Krong Preah Sihanouk", "Krong Ta Khmau", "Kampong Speu"),
+ cities = listOf("Phnom Penh", "Battambang", "Siem Reap", "Sihanoukville", "Kampong Cham", "Krong Preah Sihanouk", "Krong Ta Khmau", "Kampong Speu")
+ )
+ "$region/Jakarta",
+ "$region/Makassar",
+ "$region/Jayapura" -> Zone(
+ zoneIdString, region,
+ country = listOf("Indonesia", "ID", "\uD83C\uDDEE\uD83C\uDDE9"),
+ states = listOf("Jakarta", "East Java", "West Java", "Central Java", "Banten", "West Kalimantan", "Central Kalimantan", "East Kalimantan", "South Kalimantan", "North Sumatra", "South Sumatra"),
+ cities = listOf("Jakarta", "Surabaya", "Bandung", "Medan", "Semarang", "Bekasi", "Tangerang", "Makassar", "Depok", "Palembang")
+ )
+ "$region/Vientiane" -> Zone(
+ zoneIdString, region,
+ country = listOf("Laos", "LA", "\uD83C\uDDF1\uD83C\uDDE6"),
+ states = listOf("Vientiane", "Savannakhet", "Luang Prabang", "Champasak", "Bokeo", "Houaphanh", "Khammouane", "Attapeu", "Xiangkhouang"),
+ cities = listOf("Vientiane", "Pakse", "Luang Prabang", "Savannakhet", "Thakhek", "Muang Xay")
+ )
+ "$region/Kuala_Lumpur",
+ "$region/Kuching" -> Zone(
+ zoneIdString, region,
+ country = listOf("Malaysia", "MY", "\uD83C\uDDF2\uD83C\uDDFE"),
+ states = listOf("Johor", "Kedah", "Kelantan", "Malacca", "Negeri Sembilan", "Pahang", "Perak", "Perlis", "Penang", "Sabah", "Sarawak", "Selangor", "Terengganu", "Labuan"),
+ cities = listOf("Kuala Lumpur", "George Town", "Ipoh", "Johor Bahru", "Kuching", "Kota Kinabalu", "Shah Alam", "Malacca", "Alor Setar", "Seremban", "Klang", "Rawang"),
+ )
+ "$region/Yangon" -> Zone(
+ zoneIdString, region,
+ country = listOf("Myanmar", "MM", "\uD83C\uDDF2\uD83C\uDDF2"),
+ states = listOf("Yangon", "Mandalay", "Ayeyarwady", "Sagaing", "Bago", "Magway", "Tanintharyi", "Kachin", "Kayah", "Kayin", "Chin", "Mon", "Rakhine", "Shan"),
+ cities = listOf("Yangon", "Mandalay", "Naypyidaw", "Mawlamyine", "Bago", "Pathein", "Monywa", "Meiktila", "Sittwe", "Myeik", "Pakokku")
+ )
+ "$region/Manila" -> Zone(
+ zoneIdString, region,
+ country = listOf("Philippines", "PH", "\uD83C\uDDF5\uD83C\uDDED"),
+ states = listOf("Metro Manila", "Calabarzon", "Central Luzon", "Central Visayas", "Western Visayas", "Davao Region", "Northern Mindanao", "Soccsksargen", "Cagayan Valley", "Caraga", "Cordillera Administrative Region", "Ilocos Region", "Mimaropa", "Eastern Visayas", "Zamboanga Peninsula", "Autonomous Region in Muslim Mindanao"),
+ cities = listOf("Manila", "Quezon City", "Davao City", "Cebu City", "Zamboanga City", "Taguig", "Antipolo", "Cagayan de Oro", "Cainta", "Pasig")
+ )
+ "$region/Singapore" -> Zone(
+ zoneIdString, region,
+ country = listOf("Singapore", "SG", "\uD83C\uDDF8\uD83C\uDDEC"),
+ states = listOf("Singapore"),
+ cities = listOf("Singapore")
+ )
+ "$region/Bangkok" -> Zone(
+ zoneIdString, region,
+ country = listOf("Thailand", "TH", "\uD83C\uDDF9\uD83C\uDDED"),
+ states = listOf("Bangkok", "Chiang Mai", "Nakhon Ratchasima", "Khon Kaen", "Udon Thani", "Nakhon Sawan", "Chonburi", "Surat Thani", "Phuket", "Ayutthaya"),
+ cities = listOf("Bangkok", "Nonthaburi", "Nakhon Ratchasima", "Chiang Mai", "Hat Yai", "Udon Thani", "Pak Kret", "Khon Kaen", "Nakhon Pathom", "Chiang Rai", "Nakhon Sawan", "Ubon Ratchathani", "Korat", "Surat Thani", "Phuket", "Samut Prakan", "Rayong", "Nakhon Si Thammarat", "Ayutthaya", "Chonburi")
+ )
+ "$region/Dili" -> Zone(
+ zoneIdString, region,
+ country = listOf("Timor-Leste", "TL", "\uD83C\uDDF9\uD83C\uDDF1"),
+ states = listOf("Dili", "Baucau", "Lautém", "Viqueque", "Manufahi", "Manatuto", "Aileu", "Bobonaro", "Ermera"),
+ cities = listOf("Dili", "Baucau", "Lautém", "Viqueque", "Suai", "Manatuto")
+ )
+ "$region/Ho_Chi_Minh",
+ "$region/Saigon" -> Zone(
+ zoneIdString, region,
+ country = listOf("Vietnam", "VN", "\uD83C\uDDFB\uD83C\uDDF3"),
+ states = listOf("Ho Chi Minh City", "Hanoi", "Hai Phong", "Can Tho", "Da Nang", "Bien Hoa", "Vung Tau", "Nha Trang", "Hue", "Buon Ma Thuot", "Pleiku", "My Tho"),
+ cities = listOf("Ho Chi Minh City", "Hanoi", "Hai Phong", "Can Tho", "Da Nang", "Bien Hoa", "Vung Tau", "Nha Trang", "Hue", "Buon Ma Thuot", "Pleiku", "My Tho")
+ )
+ // East Asia
+ "$region/Shanghai",
+ "$region/Urumqi" -> Zone(
+ zoneIdString, region,
+ country = listOf("China", "CN", "\uD83C\uDDE8\uD83C\uDDF3"),
+ states = listOf("Shanghai", "Beijing", "Guangdong", "Zhejiang"),
+ cities = listOf("Shanghai", "Beijing", "Guangzhou", "Shenzhen")
+ )
+ "$region/Tokyo" -> Zone(
+ zoneIdString, region,
+ country = listOf("Japan", "JP", "\uD83C\uDDEF\uD83C\uDDF5"),
+ states = listOf("Tokyo", "Osaka", "Kyoto", "Kanagawa"),
+ cities = listOf("Tokyo", "Osaka", "Kyoto", "Yokohama")
+ )
+ "$region/Seoul" -> Zone(
+ zoneIdString, region,
+ country = listOf("South Korea", "KR", "\uD83C\uDDF0\uD83C\uDDF7"),
+ states = listOf("Seoul", "Busan", "Gyeonggi", "Incheon"),
+ cities = listOf("Seoul", "Busan", "Incheon", "Daegu")
+ )
+ "$region/Pyongyang" -> Zone(
+ zoneIdString, region,
+ country = listOf("North Korea", "KP", "\uD83C\uDDF0\uD83C\uDDF5"),
+ states = listOf("Pyongyang", "South Pyongan", "North Hwanghae", "South Hamgyong"),
+ cities = listOf("Pyongyang", "Hamhung", "Nampo", "Wonsan")
+ )
+ "$region/Ulaanbaatar" -> Zone(
+ zoneIdString, region,
+ country = listOf("Mongolia", "MN", "\uD83C\uDDF2\uD83C\uDDF3"),
+ states = listOf("Ulaanbaatar", "Darkhan-Uul", "Orkhon", "Selenge"),
+ cities = listOf("Ulaanbaatar", "Erdenet", "Darkhan", "Choibalsan")
+ )
+ "$region/Taipei" -> Zone(
+ zoneIdString, region,
+ country = listOf("Taiwan", "TW", "\uD83C\uDDF9\uD83C\uDDFC"),
+ states = listOf("Taipei", "New Taipei", "Kaohsiung", "Taichung"),
+ cities = listOf("Taipei", "New Taipei", "Kaohsiung", "Taichung")
+ )
+ // South Asia
+ "$region/Kabul" -> Zone(
+ zoneIdString, region,
+ country = listOf("Afghanistan", "AF", "\uD83C\uDDE6\uD83C\uDDEB"),
+ states = emptyList(),
+ cities = listOf("Kabul", "Kandahar", "Herat", "Mazar-i-Sharif", "Jalalabad", "Kunduz", "Lashkar Gah", "Taloqan", "Puli Khumri", "Ghazni"),
+ )
+ "$region/Dhaka" -> Zone(
+ zoneIdString, region,
+ country = listOf("Bangladesh", "BD", "\uD83C\uDDE7\uD83C\uDDE9"),
+ states = emptyList(),
+ cities = listOf("Dhaka", "Chittagong", "Khulna", "Rajshahi", "Sylhet", "Barisal", "Rangpur", "Comilla", "Narayanganj", "Gazipur"),
+ )
+ "$region/Thimphu" -> Zone(
+ zoneIdString, region,
+ country = listOf("Bhutan", "BT", "\uD83C\uDDE7\uD83C\uDDF9"),
+ states = emptyList(),
+ cities = listOf("Thimphu", "Phuntsholing", "Paro", "Punakha", "Wangdue Phodrang", "Trashigang", "Mongar", "Trongsa", "Gelephu", "Samdrup Jongkhar"),
+ )
+ "$region/Kolkata" -> Zone(
+ zoneIdString, region,
+ country = listOf("India", "IN", "\uD83C\uDDEE\uD83C\uDDF3"),
+ states = emptyList(),
+ cities = listOf("Mumbai", "New Delhi", "Bengaluru", "Kolkata", "Chennai", "Hyderabad", "Pune", "Ahmedabad", "Surat", "Jaipur"),
+ )
+ "$region/Kathmandu" -> Zone(
+ zoneIdString, region,
+ country = listOf("Nepal", "NP", "\uD83C\uDDF3\uD83C\uDDF5"),
+ states = emptyList(),
+ cities = listOf("Kathmandu", "Pokhara", "Lalitpur", "Biratnagar", "Birgunj", "Dharan", "Bharatpur", "Bhaktapur", "Butwal", "Dhangadhi"),
+ )
+ "$region/Karachi" -> Zone(
+ zoneIdString, region,
+ country = listOf("Pakistan", "PK", "\uD83C\uDDF5\uD83C\uDDF0"),
+ states = emptyList(),
+ cities = listOf("Karachi", "Lahore", "Faisalabad", "Rawalpindi", "Gujranwala", "Peshawar", "Multan", "Hyderabad", "Islamabad", "Quetta"),
+ )
+ "$region/Colombo" -> Zone(
+ zoneIdString, region,
+ country = listOf("Sri Lanka", "LK", "\uD83C\uDDF1\uD83C\uDDF0"),
+ states = emptyList(),
+ cities = listOf("Colombo", "Dehiwala-Mount Lavinia", "Moratuwa", "Jaffna", "Negombo", "Pita Kotte", "Kotte", "Kandy", "Trincomalee", "Kalmunai")
+ )
+ else -> Zone(zoneIdString, region)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/mapper/AustraliaZoneIdToZoneMapper.kt b/composeApp/src/commonMain/kotlin/data/kairos/mapper/AustraliaZoneIdToZoneMapper.kt
new file mode 100644
index 0000000..8a70e66
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/mapper/AustraliaZoneIdToZoneMapper.kt
@@ -0,0 +1,71 @@
+package data.kairos.mapper
+
+import data.kairos.model.Region
+import data.kairos.model.Zone
+import data.utils.Mapper
+
+class AustraliaZoneIdToZoneMapper : Mapper, List> {
+ companion object {
+ private val region = Region.Australia
+ }
+
+ override fun invoke(input: List): List = input.map { zoneIdString ->
+ when (zoneIdString) {
+ "$region/Adelaide" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = listOf("South Australia"),
+ cities = listOf("Adelaide")
+ )
+ "$region/Brisbane" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = listOf("Queensland"),
+ cities = listOf("Brisbane")
+ )
+ "$region/Broken_Hill" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = listOf("South Australia", "New South Wales"),
+ cities = listOf("Broken Hill")
+ )
+ "$region/Darwin" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = listOf("Northern Territory"),
+ cities = listOf("Darwin")
+ )
+ "$region/Hobart" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = listOf("Tasmania"),
+ cities = listOf("Hobart")
+ )
+ "$region/Lord_Howe" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = emptyList(),
+ cities = listOf("Lord Howe Island")
+ )
+ "$region/Melbourne" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = listOf("Victoria"),
+ cities = listOf("Melbourne")
+ )
+ "$region/Perth" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = emptyList(),
+ cities = listOf("Perth")
+ )
+ "$region/Sydney" -> Zone(
+ zoneIdString, region,
+ country = listOf("Australia", "AU", "\uD83C\uDDE6\uD83C\uDDFA"),
+ states = listOf("New South Wales"),
+ cities = listOf("Sydney")
+ )
+ else -> Zone(zoneIdString, region)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/mapper/EuropeZoneIdToZoneMapper.kt b/composeApp/src/commonMain/kotlin/data/kairos/mapper/EuropeZoneIdToZoneMapper.kt
new file mode 100644
index 0000000..544f3dc
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/mapper/EuropeZoneIdToZoneMapper.kt
@@ -0,0 +1,277 @@
+package data.kairos.mapper
+
+import data.kairos.model.Region
+import data.kairos.model.Zone
+import data.utils.Mapper
+
+class EuropeZoneIdToZoneMapper : Mapper, List> {
+ companion object {
+ private val region = Region.Europe
+ }
+
+ override fun invoke(input: List): List = input.map { zoneIdString ->
+ when (zoneIdString) {
+ "$region/Lisbon" -> Zone(
+ zoneIdString, region,
+ country = listOf("Portugal", "PT", "\uD83C\uDDF5\uD83C\uDDF9"),
+ states = emptyList(),
+ cities = listOf("Lisbon", "Porto", "Vila Nova de Gaia", "Amadora", "Braga", "Setúbal", "Coimbra", "Funchal", "Almada", "Aveiro")
+ )
+ "$region/Madrid" -> Zone(
+ zoneIdString, region,
+ country = listOf("Spain", "ES", "\uD83C\uDDEA\uD83C\uDDF8"),
+ states = emptyList(),
+ cities = listOf("Madrid", "Barcelona", "Valencia", "Seville", "Zaragoza", "Málaga", "Murcia", "Palma", "Las Palmas", "Bilbao")
+ )
+ "$region/Paris" -> Zone(
+ zoneIdString, region,
+ country = listOf("France", "FR", "\uD83C\uDDEB\uD83C\uDDF7"),
+ states = emptyList(),
+ cities = listOf("Paris", "Marseille", "Lyon", "Toulouse", "Nice", "Nantes", "Strasbourg", "Montpellier", "Bordeaux", "Lille")
+ )
+ "$region/Andorra" -> Zone(
+ zoneIdString, region,
+ country = listOf("Andorra", "AD", "\uD83C\uDDE6\uD83C\uDDE9"),
+ states = emptyList(),
+ cities = listOf("Andorra la Vella", "Escaldes-Engordany", "Encamp", "Sant Julià de Lòria", "La Massana", "Santa Coloma", "Ordino", "Canillo")
+ )
+ "$region/Monaco" -> Zone(
+ zoneIdString, region,
+ country = listOf("Monaco", "MC", "\uD83C\uDDF2\uD83C\uDDE8"),
+ states = emptyList(),
+ cities = listOf("Monaco", "Monte Carlo", "La Condamine", "Fontvieille")
+ )
+ "$region/Luxembourg" -> Zone(
+ zoneIdString, region,
+ country = listOf("Luxembourg", "LU", "\uD83C\uDDF1\uD83C\uDDFA"),
+ states = emptyList(),
+ cities = listOf("Luxembourg City", "Esch-sur-Alzette", "Differdange", "Dudelange", "Ettelbruck")
+ )
+ "$region/Brussels" -> Zone(
+ zoneIdString, region,
+ country = listOf("Belgium", "BE", "\uD83C\uDDE7\uD83C\uDDEA"),
+ states = emptyList(),
+ cities = listOf("Brussels", "Antwerp", "Ghent", "Charleroi", "Liège", "Bruges", "Namur", "Leuven", "Mechelen", "Aalst")
+ )
+ "$region/Amsterdam" -> Zone(
+ zoneIdString, region,
+ country = listOf("Netherlands", "NL", "\uD83C\uDDF3\uD83C\uDDF1"),
+ states = listOf("Drenthe", "Flevoland", "Friesland", "Gelderland", "Groningen", "Limburg", "North Brabant", "North Holland", "Overijssel", "Utrecht", "Zeeland", "South Holland"),
+ cities = listOf("Amsterdam", "Rotterdam", "The Hague", "Utrecht", "Eindhoven", "Tilburg", "Groningen", "Almere", "Breda", "Nijmegen", "Enschede", "Haarlem", "Arnhem", "Zaanstad", "Amersfoort", "Apeldoorn", "Zwolle", "Maastricht", "Dordrecht", "Leiden")
+ )
+ "$region/Berlin" -> Zone(
+ zoneIdString, region,
+ country = listOf("Germany", "DE", "\uD83C\uDDE9\uD83C\uDDEA"),
+ states = listOf("Brandenburg", "Berlin", "Bavaria", "Saxony Anhalt", "Mecklenburg Vorpommern", "Hamburg", "Saxony", "North Rhine Westphalia", "Schleswig Holstein", "Bremen", "Baden Württemberg", "Hesse", "Lower Saxony", "Rhineland Palatinate", "Saarland", "Thuringia"),
+ cities = listOf("Berlin", "Hamburg", "Munich", "Cologne", "Frankfurt", "Stuttgart", "Düsseldorf", "Dortmund", "Essen", "Leipzig")
+ )
+ "$region/Prague" -> Zone(
+ zoneIdString, region,
+ country = listOf("Czech Republic", "Czechia", "CZ", "\uD83C\uDDE8\uD83C\uDDFF"),
+ states = emptyList(),
+ cities = listOf("Prague", "Brno", "Ostrava", "Plzeň", "Liberec", "Olomouc", "České Budějovice", "Ústí nad Labem", "Hradec Králové", "Pardubice")
+ )
+ "$region/Zurich" -> Zone(
+ zoneIdString, region,
+ country = listOf("Switzerland", "CH", "\uD83C\uDDE8\uD83C\uDDED"),
+ states = emptyList(),
+ cities = listOf("Zurich", "Geneva", "Basel", "Lausanne", "Bern", "Winterthur", "Lucerne", "St. Gallen", "Lugano", "Biel/Bienne")
+ )
+ "$region/Vaduz" -> Zone(
+ zoneIdString, region,
+ country = listOf("Liechtenstein", "LI", "\uD83C\uDDF1\uD83C\uDDEE"),
+ states = emptyList(),
+ cities = listOf("Vaduz", "Schaan", "Triesen", "Balzers", "Eschen", "Mauren", "Ruggell", "Gamprin", "Schellenberg", "Planken")
+ )
+ "$region/Vienna" -> Zone(
+ zoneIdString, region,
+ country = listOf("Austria", "AT", "\uD83C\uDDE6\uD83C\uDDF9"),
+ states = emptyList(),
+ cities = listOf("Vienna", "Graz", "Linz", "Salzburg", "Innsbruck", "Klagenfurt", "Villach", "Wels", "Sankt Pölten", "Dornbirn")
+ )
+ "$region/London",
+ "$region/Isle_of_Man",
+ "$region/Jersey" -> Zone(
+ zoneIdString, region,
+ country = listOf("United Kingdom", "UK", "\uD83C\uDDEC\uD83C\uDDE7"),
+ states = emptyList(),
+ cities = emptyList()
+ )
+ "$region/Dublin" -> Zone(
+ zoneIdString, region,
+ country = listOf("Ireland", "IE", "\uD83C\uDDEE\uD83C\uDDEA"),
+ states = emptyList(),
+ cities = listOf("Dublin", "Cork", "Galway", "Limerick", "Waterford", "Drogheda", "Dundalk", "Sligo", "Bray", "Navan")
+ )
+ "$region/Copenhagen" -> Zone(
+ zoneIdString, region,
+ country = listOf("Denmark", "DK", "\uD83C\uDDE9\uD83C\uDDF0"),
+ states = emptyList(),
+ cities = emptyList()
+ )
+ "$region/Oslo" -> Zone(
+ zoneIdString, region,
+ country = listOf("Norway", "NO", "\uD83C\uDDF3\uD83C\uDDF4"),
+ states = emptyList(),
+ cities = listOf("Oslo", "Bergen", "Trondheim", "Stavanger", "Drammen", "Fredrikstad", "Kristiansand", "Sandnes", "Tromsø", "Sarpsborg")
+ )
+ "$region/Stockholm" -> Zone(
+ zoneIdString, region,
+ country = listOf("Sweden", "SE", "\uD83C\uDDF8\uD83C\uDDEA"),
+ states = emptyList(),
+ cities = listOf("Stockholm", "Gothenburg", "Malmö", "Uppsala", "Sollentuna", "Västerås", "Örebro", "Linköping", "Helsingborg", "Jönköping")
+ )
+ "$region/Helsinki" -> Zone(
+ zoneIdString, region,
+ country = listOf("Finland", "FI", "\uD83C\uDDEB\uD83C\uDDEE"),
+ states = emptyList(),
+ cities = listOf("Helsinki", "Espoo", "Tampere", "Vantaa", "Oulu", "Turku", "Jyväskylä", "Kuopio", "Lahti", "Pori")
+ )
+ "$region/Malta" -> Zone(
+ zoneIdString, region,
+ country = listOf("Malta", "MT", "\uD83C\uDDF2\uD83C\uDDF9"),
+ states = emptyList(),
+ cities = listOf("Valletta", "Birkirkara", "Mosta", "Qormi", "Żabbar", "San Ġwann", "Żejtun", "Sliema", "Luqa", "Gżira")
+ )
+ "$region/Rome" -> Zone(
+ zoneIdString, region,
+ country = listOf("Italy", "IT", "\uD83C\uDDEE\uD83C\uDDF9"),
+ states = emptyList(),
+ cities = listOf("Rome", "Milan", "Naples", "Turin", "Palermo", "Genoa", "Bologna", "Florence", "Bari", "Catania")
+ )
+ "$region/San_Marino" -> Zone(
+ zoneIdString, region,
+ country = listOf("San Marino", "SM", "\uD83C\uDDF8\uD83C\uDDF2"),
+ states = emptyList(),
+ cities = listOf("San Marino", "Serravalle", "Borgo Maggiore", "Domagnano", "Fiorentino", "Acquaviva", "Chiesanuova", "Montegiardino", "San Leo")
+ )
+ "$region/Vatican" -> Zone(
+ zoneIdString, region,
+ country = listOf("Vatican City", "VA", "\uD83C\uDDFB\uD83C\uDDE6"),
+ states = emptyList(),
+ cities = listOf("Vatican City")
+ )
+ "$region/Ljubljana" -> Zone(
+ zoneIdString, region,
+ country = listOf("Slovenia", "SI", "\uD83C\uDDF8\uD83C\uDDEE"),
+ states = emptyList(),
+ cities = listOf("Ljubljana", "Maribor", "Celje", "Kranj", "Velenje", "Koper", "Novo Mesto", "Ptuj", "Trbovlje", "Kamnik")
+ )
+ "$region/Zagreb" -> Zone(
+ zoneIdString, region,
+ country = listOf("Croatia", "HR", "\uD83C\uDDED\uD83C\uDDF7"),
+ states = emptyList(),
+ cities = listOf("Zagreb", "Split", "Rijeka", "Osijek", "Zadar", "Slavonski Brod", "Pula", "Karlovac", "Sisak", "Varaždin")
+ )
+ "$region/Sarajevo" -> Zone(
+ zoneIdString, region,
+ country = listOf("Bosnia and Herzegovina", "BA", "\uD83C\uDDE7\uD83C\uDDE6"),
+ states = emptyList(),
+ cities = listOf("Sarajevo", "Banja Luka", "Tuzla", "Zenica", "Mostar", "Prijedor", "Brčko", "Bihać", "Bugojno", "Trebinje")
+ )
+ "$region/Podgorica" -> Zone(
+ zoneIdString, region,
+ country = listOf("Montenegro", "ME", "\uD83C\uDDF2\uD83C\uDDEA"),
+ states = emptyList(),
+ cities = listOf("Podgorica", "Nikšić", "Herceg Novi", "Bar", "Budva", "Kotor", "Cetinje", "Berane", "Pljevlja", "Ulcinj")
+ )
+ "$region/Tirane" -> Zone(
+ zoneIdString, region,
+ country = listOf("Albania", "AL", "\uD83C\uDDE6\uD83C\uDDF1"),
+ states = emptyList(),
+ cities = listOf("Tirana", "Durrës", "Vlorë", "Shkodër", "Fier", "Kamëz", "Korçë", "Fier-Çifçi", "Berat", "Lushnjë")
+ )
+ "$region/Belgrade" -> Zone(
+ zoneIdString, region,
+ country = listOf("Serbia", "Kosovo", "RS", "XK"),
+ states = emptyList(),
+ cities = listOf("Belgrade", "Novi Sad", "Niš", "Kragujevac", "Subotica", "Zrenjanin", "Pančevo", "Čačak", "Novi Pazar", "Kraljevo", "Pristina", "Prizren", "Peć", "Gjakova", "Mitrovica", "Ferizaj", "Gjilan", "Kosovska Mitrovica", "Podujevo", "Vučitrn")
+ )
+ "$region/Skopje" -> Zone(
+ zoneIdString, region,
+ country = listOf("North Macedonia", "MK", "\uD83C\uDDF2\uD83C\uDDF0"),
+ states = emptyList(),
+ cities = listOf("Skopje", "Bitola", "Kumanovo", "Prilep", "Tetovo", "Veles", "Ohrid", "Gostivar", "Štip", "Strumica")
+ )
+ "$region/Athens" -> Zone(
+ zoneIdString, region,
+ country = listOf("Greece", "GR", "\uD83C\uDDEC\uD83C\uDDF7"),
+ states = emptyList(),
+ cities = listOf("Athens", "Thessaloniki", "Patras", "Heraklion", "Larissa", "Volos", "Ioannina", "Chania", "Chalcis", "Serres")
+ )
+ "$region/Sofia" -> Zone(
+ zoneIdString, region,
+ country = listOf("Bulgaria", "BG", "\uD83C\uDDE7\uD83C\uDDEC"),
+ states = emptyList(),
+ cities = listOf("Sofia", "Plovdiv", "Varna", "Burgas", "Ruse", "Stara Zagora", "Pleven", "Sliven", "Dobrich", "Shumen")
+ )
+ "$region/Bucharest" -> Zone(
+ zoneIdString, region,
+ country = listOf("Romania", "RO", "\uD83C\uDDF7\uD83C\uDDF4"),
+ states = emptyList(),
+ cities = listOf("Bucharest", "Cluj-Napoca", "Timișoara", "Iași", "Craiova", "Constanța", "Galați", "Brașov", "Ploiești", "Oradea")
+ )
+ "$region/Chisinau" -> Zone(
+ zoneIdString, region,
+ country = listOf("Moldova", "MD", "\uD83C\uDDF2\uD83C\uDDE9"),
+ states = emptyList(),
+ cities = listOf("Chișinău", "Tiraspol", "Bălți", "Bender", "Rîbnița", "Cahul", "Ungheni", "Soroca", "Orhei", "Dubăsari")
+ )
+ "$region/Kiev" -> Zone(
+ zoneIdString, region,
+ country = listOf("Ukraine", "UA", "\uD83C\uDDFA\uD83C\uDDE6"),
+ states = emptyList(),
+ cities = listOf("Kyiv", "Kharkiv", "Odesa", "Dnipro", "Donetsk", "Zaporizhzhia", "Lviv", "Kryvyi Rih", "Mykolaiv", "Mariupol")
+ )
+ "$region/Minsk" -> Zone(
+ zoneIdString, region,
+ country = listOf("Belarus", "BY", "\uD83C\uDDE7\uD83C\uDDFE"),
+ states = emptyList(),
+ cities = listOf("Minsk", "Gomel", "Mogilev", "Vitebsk", "Hrodna", "Brest", "Babruysk", "Baranovichi", "Borisov", "Pinsk")
+ )
+ "$region/Warsaw" -> Zone(
+ zoneIdString, region,
+ country = listOf("Poland", "PL", "\uD83C\uDDF5\uD83C\uDDF1"),
+ states = emptyList(),
+ cities = listOf("Warsaw", "Kraków", "Łódź", "Wrocław", "Poznań", "Gdańsk", "Szczecin", "Bydgoszcz", "Lublin", "Katowice")
+ )
+ "$region/Vilnius" -> Zone(
+ zoneIdString, region,
+ country = listOf("Lithuania", "LT", "\uD83C\uDDF1\uD83C\uDDF9"),
+ states = emptyList(),
+ cities = listOf("Vilnius", "Kaunas", "Klaipėda", "Šiauliai", "Panevėžys", "Alytus", "Marijampolė", "Mažeikiai", "Jonava", "Utena")
+ )
+ "$region/Riga" -> Zone(
+ zoneIdString, region,
+ country = listOf("Latvia", "LV", "\uD83C\uDDF1\uD83C\uDDFB"),
+ states = emptyList(),
+ cities = listOf("Riga", "Daugavpils", "Liepāja", "Jelgava", "Jūrmala", "Ventspils", "Rēzekne", "Valmiera", "Jēkabpils", "Ogre")
+ )
+ "$region/Tallinn" -> Zone(
+ zoneIdString, region,
+ country = listOf("Estonia", "EE", "\uD83C\uDDEA\uD83C\uDDEA"),
+ states = emptyList(),
+ cities = listOf("Tallinn", "Tartu", "Narva", "Pärnu", "Kohtla-Järve", "Viljandi", "Rakvere", "Maardu", "Sillamäe", "Kuressaare")
+ )
+ "$region/Bratislava" -> Zone(
+ zoneIdString, region,
+ country = listOf("Slovakia", "SK", "\uD83C\uDDF8\uD83C\uDDF0"),
+ states = emptyList(),
+ cities = listOf("Bratislava", "Košice", "Prešov", "Žilina", "Nitra", "Banská Bystrica", "Trnava", "Martin", "Trenčín", "Poprad")
+ )
+ "$region/Ljubljana" -> Zone(
+ zoneIdString, region,
+ country = listOf("Slovenia", "SI", "\uD83C\uDDF8\uD83C\uDDEE"),
+ states = emptyList(),
+ cities = listOf("Ljubljana", "Maribor", "Celje", "Kranj", "Velenje", "Koper", "Novo Mesto", "Ptuj", "Trbovlje", "Kamnik")
+ )
+ "$region/Moscow" -> Zone(
+ zoneIdString, region,
+ country = listOf("Russia", "RU", "\uD83C\uDDF7\uD83C\uDDFA"),
+ states = emptyList(),
+ cities = listOf("Moscow", "Saint Petersburg", "Novosibirsk", "Yekaterinburg", "Nizhny Novgorod", "Kazan", "Chelyabinsk", "Omsk", "Samara", "Rostov-on-Don")
+ )
+ else -> Zone(zoneIdString, region)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/mapper/PacificZoneIdToZoneMapper.kt b/composeApp/src/commonMain/kotlin/data/kairos/mapper/PacificZoneIdToZoneMapper.kt
new file mode 100644
index 0000000..1e18e76
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/mapper/PacificZoneIdToZoneMapper.kt
@@ -0,0 +1,23 @@
+package data.kairos.mapper
+
+import data.kairos.model.Region
+import data.kairos.model.Zone
+import data.utils.Mapper
+
+class PacificZoneIdToZoneMapper : Mapper, List> {
+ companion object {
+ private val region = Region.Pacific
+ }
+
+ override fun invoke(input: List): List = input.map { zoneIdString ->
+ when (zoneIdString) {
+ "$region/Auckland" -> Zone(
+ zoneIdString, region,
+ country = listOf("New Zealand", "NZ", "\uD83C\uDDF3\uD83C\uDDFF"),
+ states = listOf("Auckland", "Taranaki", "Hawke's Bay", "Wellington", "Nelson", "Marlborough", "Westland", "Canterbury", "Otago"),
+ cities = listOf("Auckland", "Christchurch", "Wellington")
+ )
+ else -> Zone(zoneIdString, region)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/model/Region.kt b/composeApp/src/commonMain/kotlin/data/kairos/model/Region.kt
new file mode 100644
index 0000000..49e4b00
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/model/Region.kt
@@ -0,0 +1,19 @@
+package data.kairos.model
+
+enum class Region {
+ Africa,
+ America,
+ Antarctica,
+ Arctic,
+ Asia,
+ Atlantic,
+ Australia,
+ Brazil,
+ Canada,
+ Chile,
+ Europe,
+ Indian,
+ Mexico,
+ Pacific,
+ US
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/kairos/model/Zone.kt b/composeApp/src/commonMain/kotlin/data/kairos/model/Zone.kt
new file mode 100644
index 0000000..046a347
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/kairos/model/Zone.kt
@@ -0,0 +1,31 @@
+package data.kairos.model
+
+import data.utils.DateFormat
+import data.utils.now
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.format
+import randomUUID
+
+data class Zone(
+ val zoneIdString: String,
+ val region: Region,
+ val id: String = randomUUID(),
+ private val country: List = listOf(),
+ private val states: List = listOf(),
+ private val cities: List = listOf()
+) {
+ val timeZone get() = TimeZone.of(zoneIdString)
+ val zone get() = zoneIdString.split("/").first()
+ val city get() = zoneIdString.split("/").last().replace(Regex("[_-]"), " ")
+ private val keywords: List get() = (listOf(region.name, zone, city) + country + states + cities)
+
+ fun doesMatchSearchQuery(query: String): Boolean {
+ return keywords.any { it.contains(query, ignoreCase = true) }
+ }
+
+ fun localTime(): String {
+ val now: LocalDateTime = LocalDateTime.now(timeZone)
+ return now.format(DateFormat.twelveHour)
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/location/LocationRepository.kt b/composeApp/src/commonMain/kotlin/data/location/LocationRepository.kt
new file mode 100644
index 0000000..8862bf1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/location/LocationRepository.kt
@@ -0,0 +1,67 @@
+package data.location
+
+import app.cash.sqldelight.coroutines.asFlow
+import app.cash.sqldelight.coroutines.mapToList
+import app.cash.sqldelight.coroutines.mapToOne
+import data.location.model.Location
+import di.TwilightDatabaseWrapper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+interface LocationRepository {
+ fun getLocation(location: Location): Flow
+ fun getLocations(): Flow>
+ suspend fun addLocation(location: Location)
+ suspend fun updateLocation(location: Location)
+ suspend fun deleteLocation(id: String)
+ suspend fun deleteLocations()
+}
+
+internal class LocationRepositoryImpl : LocationRepository, KoinComponent {
+ private val twilightDatabase: TwilightDatabaseWrapper by inject()
+ private val queries get() = twilightDatabase.instance?.locationEntityQueries
+
+ override fun getLocation(location: Location): Flow =
+ queries?.selectById(
+ id = location.id,
+ mapper = { id, label, regionName, zoneIdString ->
+ Location(id, label, regionName, zoneIdString)
+ }
+ )?.asFlow()?.mapToOne(Dispatchers.Default) ?: flowOf(null)
+
+ override fun getLocations(): Flow> =
+ queries?.selectAll(
+ mapper = { id, label, regionName, zoneIdString ->
+ Location(id, label, regionName, zoneIdString)
+ }
+ )?.asFlow()?.mapToList(Dispatchers.Default) ?: flowOf(emptyList())
+
+ override suspend fun addLocation(location: Location) {
+ queries?.insertItem(
+ id = location.id,
+ label = location.name,
+ regionName = location.regionName,
+ zoneIdString = location.zoneId
+ )
+ }
+
+ override suspend fun updateLocation(location: Location) {
+ queries?.update(
+ label = location.name,
+ regionName = location.regionName,
+ zoneIdString = location.zoneId,
+ id = location.id
+ )
+ }
+
+ override suspend fun deleteLocation(id: String) {
+ queries?.deleteById(id)
+ }
+
+ override suspend fun deleteLocations() {
+ queries?.deleteAll()
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/location/model/Location.kt b/composeApp/src/commonMain/kotlin/data/location/model/Location.kt
new file mode 100644
index 0000000..efff780
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/location/model/Location.kt
@@ -0,0 +1,13 @@
+package data.location.model
+
+import kotlinx.datetime.TimeZone
+import randomUUID
+
+data class Location(
+ val id: String = randomUUID(),
+ val name: String,
+ val regionName: String,
+ val zoneId: String,
+) {
+ val zone get() = TimeZone.of(zoneId)
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/locationform/LocationFormRepository.kt b/composeApp/src/commonMain/kotlin/data/locationform/LocationFormRepository.kt
new file mode 100644
index 0000000..11387ce
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/locationform/LocationFormRepository.kt
@@ -0,0 +1,80 @@
+package data.locationform
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import data.locationform.model.NewLocationData
+import data.utils.LocalDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import org.koin.core.qualifier.named
+
+interface LocationFormRepository {
+ suspend fun saveID(value: String)
+ suspend fun saveName(value: String)
+ suspend fun saveRegionName(value: String)
+ suspend fun saveZoneId(value: String)
+
+ fun getID(): Flow
+ fun getName(): Flow
+ fun getRegionName(): Flow
+ fun getZoneId(): Flow
+
+ fun observeLocation(): Flow
+
+ suspend fun clear()
+}
+
+internal class LocationFormRepositoryImpl : LocationFormRepository, KoinComponent {
+
+ companion object {
+ const val KEY_ID = "vrYeumMjGctxQbwKqapb"
+ const val KEY_NAME = "jpdDoCQTqeJrGsnaQcBW"
+ const val KEY_REGION_NAME = "VzDMLXFKRseRmWRBEFuX"
+ const val KEY_ZONE_ID = "XPXkEZAfgQPLvDsvaHAZ"
+ }
+
+ private val dataStore: DataStore by inject(qualifier = named(LocalDataStore.CreateNewLocation.name))
+
+ override suspend fun saveID(value: String) {
+ dataStore.edit { it[stringPreferencesKey(KEY_ID)] = value }
+ }
+
+ override suspend fun saveName(value: String) {
+ dataStore.edit { it[stringPreferencesKey(KEY_NAME)] = value }
+ }
+
+ override suspend fun saveRegionName(value: String) {
+ dataStore.edit { it[stringPreferencesKey(KEY_REGION_NAME)] = value }
+ }
+
+ override suspend fun saveZoneId(value: String) {
+ dataStore.edit { it[stringPreferencesKey(KEY_ZONE_ID)] = value }
+ }
+
+ override fun getID(): Flow =
+ dataStore.data.map { it[stringPreferencesKey(KEY_ID)] }
+
+ override fun getName(): Flow =
+ dataStore.data.map { it[stringPreferencesKey(KEY_NAME)] }
+
+ override fun getRegionName(): Flow =
+ dataStore.data.map { it[stringPreferencesKey(KEY_REGION_NAME)] }
+
+ override fun getZoneId(): Flow =
+ dataStore.data.map { it[stringPreferencesKey(KEY_ZONE_ID)] }
+
+ override fun observeLocation(): Flow {
+ return combine(getID(), getName(), getRegionName(), getZoneId()) { id, name, regionName, zoneId ->
+ NewLocationData(id, name, regionName, zoneId)
+ }
+ }
+
+ override suspend fun clear() {
+ dataStore.edit { it.clear() }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/locationform/model/NewLocationData.kt b/composeApp/src/commonMain/kotlin/data/locationform/model/NewLocationData.kt
new file mode 100644
index 0000000..460b27c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/locationform/model/NewLocationData.kt
@@ -0,0 +1,11 @@
+package data.locationform.model
+
+data class NewLocationData(
+ val id: String? = null,
+ val label: String? = null,
+ val regionName: String? = null,
+ val zoneId: String? = null,
+) {
+ val isEditMode: Boolean
+ get() = !id.isNullOrBlank()
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/preferences/PreferencesRepository.kt b/composeApp/src/commonMain/kotlin/data/preferences/PreferencesRepository.kt
new file mode 100644
index 0000000..8049976
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/preferences/PreferencesRepository.kt
@@ -0,0 +1,67 @@
+package data.preferences
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import data.preferences.model.DateFormatPreference
+import data.preferences.model.LocationFormatPreference
+import data.preferences.model.ThemePreference
+import data.utils.LocalDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import org.koin.core.qualifier.named
+
+interface PreferencesRepository {
+ suspend fun saveTheme(value: ThemePreference)
+ suspend fun saveDateFormat(value: DateFormatPreference)
+ suspend fun saveLocationFormat(value: LocationFormatPreference)
+
+ fun getTheme(): Flow
+ fun getDateFormat(): Flow
+ fun getLocationFormat(): Flow
+ suspend fun clear()
+}
+
+internal class PreferencesRepositoryImpl : PreferencesRepository, KoinComponent {
+ companion object {
+ const val KEY_THEME = "CCYQrgtgsNRFEDuyTpWa"
+ const val KEY_DATE_FORMAT = "fqHCanepiXQpFWDhYMBk"
+ const val KEY_LOCATION_FORMAT = "gUdTtuUsuBtaxjjxCsdE"
+ }
+
+ private val dataStore: DataStore by inject(qualifier = named(LocalDataStore.Preferences.name))
+
+ override suspend fun saveTheme(value: ThemePreference) {
+ dataStore.edit { it[stringPreferencesKey(KEY_THEME)] = value.name }
+ }
+
+ override suspend fun saveDateFormat(value: DateFormatPreference) {
+ dataStore.edit { it[stringPreferencesKey(KEY_DATE_FORMAT)] = value.name }
+ }
+
+ override suspend fun saveLocationFormat(value: LocationFormatPreference) {
+ dataStore.edit { it[stringPreferencesKey(KEY_LOCATION_FORMAT)] = value.name }
+ }
+
+ override fun getTheme(): Flow =
+ dataStore.data.map {
+ ThemePreference.valueOf(it[stringPreferencesKey(KEY_THEME)] ?: ThemePreference.Default.name)
+ }
+
+ override fun getDateFormat(): Flow =
+ dataStore.data.map {
+ DateFormatPreference.valueOf(it[stringPreferencesKey(KEY_DATE_FORMAT)] ?: DateFormatPreference.Default.name)
+ }
+
+ override fun getLocationFormat(): Flow =
+ dataStore.data.map {
+ LocationFormatPreference.valueOf(it[stringPreferencesKey(KEY_LOCATION_FORMAT)] ?: LocationFormatPreference.Default.name)
+ }
+
+ override suspend fun clear() {
+ dataStore.edit { it.clear() }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/preferences/model/DateFormatPreference.kt b/composeApp/src/commonMain/kotlin/data/preferences/model/DateFormatPreference.kt
new file mode 100644
index 0000000..af44f36
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/preferences/model/DateFormatPreference.kt
@@ -0,0 +1,27 @@
+package data.preferences.model
+
+import data.utils.DateFormat
+import data.utils.now
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.format
+
+enum class DateFormatPreference {
+ TwentyFour,
+ Twelve;
+
+ companion object {
+ val Default = TwentyFour
+ }
+
+ val label: String
+ get() = when (this) {
+ TwentyFour -> "24-hour clock"
+ Twelve -> "12-hour clock"
+ }
+
+ val description: String
+ get() = when (this) {
+ TwentyFour -> LocalDateTime.now().format(DateFormat.twentyFourHour)
+ Twelve -> LocalDateTime.now().format(DateFormat.twelveHour)
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/preferences/model/LocationFormatPreference.kt b/composeApp/src/commonMain/kotlin/data/preferences/model/LocationFormatPreference.kt
new file mode 100644
index 0000000..2de2708
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/preferences/model/LocationFormatPreference.kt
@@ -0,0 +1,28 @@
+package data.preferences.model
+
+enum class LocationFormatPreference {
+ Place,
+ PlaceWithGMT,
+ Person,
+ PersonWithGMT;
+
+ companion object {
+ val Default = Place
+ }
+
+ val label: String
+ get() = when (this) {
+ Place -> "Place"
+ PlaceWithGMT -> "Place GMT Offset"
+ Person -> "Person"
+ PersonWithGMT -> "Person GMT Offset"
+ }
+
+ val description: String
+ get() = when (this) {
+ Place -> "Melbourne"
+ PlaceWithGMT -> "Melbourne GMT +10:00"
+ Person -> "Aerith Gainsborough"
+ PersonWithGMT -> "Aerith Gainsborough GMT +10:00"
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/preferences/model/ThemePreference.kt b/composeApp/src/commonMain/kotlin/data/preferences/model/ThemePreference.kt
new file mode 100644
index 0000000..c1293c9
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/preferences/model/ThemePreference.kt
@@ -0,0 +1,18 @@
+package data.preferences.model
+
+enum class ThemePreference {
+ System,
+ Light,
+ Dark;
+
+ companion object {
+ val Default = System
+ }
+
+ val label: String
+ get() = when (this) {
+ System -> "System"
+ Light -> "Light"
+ Dark -> "Dark"
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/utils/DateExtensions.kt b/composeApp/src/commonMain/kotlin/data/utils/DateExtensions.kt
new file mode 100644
index 0000000..bee1ad8
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/utils/DateExtensions.kt
@@ -0,0 +1,35 @@
+package data.utils
+
+import data.kairos.model.Zone
+import kotlinx.datetime.Clock
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.format
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
+
+fun LocalDateTime.Companion.now(timeZone: TimeZone = TimeZone.currentSystemDefault()): LocalDateTime {
+ return Clock.System.now().toLocalDateTime(timeZone)
+}
+
+fun LocalDateTime.convert(
+ from: Zone,
+ to: Zone
+): LocalDateTime {
+ return this.toInstant(from.timeZone)
+ .toLocalDateTime(to.timeZone)
+}
+
+fun LocalDate.Companion.now(): LocalDate {
+ return LocalDateTime.now().date
+}
+
+fun LocalTime.Companion.now(): LocalTime {
+ return LocalDateTime.now().time
+}
+
+fun TimeZone.localTime(): String {
+ return LocalDateTime.now(this).format(DateFormat.twelveHour)
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/utils/DateFormat.kt b/composeApp/src/commonMain/kotlin/data/utils/DateFormat.kt
new file mode 100644
index 0000000..de128ad
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/utils/DateFormat.kt
@@ -0,0 +1,34 @@
+package data.utils
+
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.format.DayOfWeekNames
+import kotlinx.datetime.format.MonthNames
+import kotlinx.datetime.format.char
+
+object DateFormat {
+ // 04:19 PM
+ val twelveHour = LocalDateTime.Format {
+ amPmHour()
+ char(':')
+ minute()
+ char(' ')
+ amPmMarker("AM", "PM")
+ }
+
+ // 22:20
+ val twentyFourHour = LocalDateTime.Format {
+ hour()
+ char(':')
+ minute()
+ }
+
+ // Thursday, 24 July
+ val dayOfWeekDayMonth = LocalDateTime.Format {
+ dayOfWeek(DayOfWeekNames.ENGLISH_FULL)
+ char(',')
+ char(' ')
+ dayOfMonth()
+ char(' ')
+ monthName(MonthNames.ENGLISH_FULL)
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/utils/LocalDataStore.kt b/composeApp/src/commonMain/kotlin/data/utils/LocalDataStore.kt
new file mode 100644
index 0000000..6fcb961
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/utils/LocalDataStore.kt
@@ -0,0 +1,8 @@
+package data.utils
+
+enum class LocalDataStore(private val url: String) {
+ Preferences("twilight.preferences_data_store"),
+ CreateNewLocation("twilight.create_new_location_data_store");
+
+ fun path() = "$url.preferences_pb"
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/data/utils/Mapper.kt b/composeApp/src/commonMain/kotlin/data/utils/Mapper.kt
new file mode 100644
index 0000000..d792b19
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/data/utils/Mapper.kt
@@ -0,0 +1,5 @@
+package data.utils
+
+interface Mapper {
+ operator fun invoke(input: Input): Output
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/di/Koin.kt b/composeApp/src/commonMain/kotlin/di/Koin.kt
new file mode 100644
index 0000000..26bd9b7
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/di/Koin.kt
@@ -0,0 +1,58 @@
+package di
+
+import com.delacrixmorgan.twilight.TwilightDatabase
+import data.location.LocationRepository
+import data.location.LocationRepositoryImpl
+import data.locationform.LocationFormRepository
+import data.locationform.LocationFormRepositoryImpl
+import data.preferences.PreferencesRepository
+import data.preferences.PreferencesRepositoryImpl
+import data.kairos.KairosRepository
+import org.koin.core.context.startKoin
+import org.koin.core.module.dsl.viewModel
+import org.koin.dsl.KoinAppDeclaration
+import org.koin.dsl.module
+import platformModule
+import ui.dashboard.settings.SettingsViewModel
+import ui.dashboard.settings.appinfo.AppInfoViewModel
+import ui.dashboard.today.TodayViewModel
+import ui.form.name.SetupNameViewModel
+import ui.form.summary.SummaryViewModel
+import ui.form.zone.SelectZoneViewModel
+
+fun initKoin(appDeclaration: KoinAppDeclaration = {}) =
+ startKoin {
+ appDeclaration()
+ modules(
+ platformModule(),
+ viewModelModule(),
+ serviceModule(),
+ repositoryModule(),
+ mapperModule()
+ )
+ }
+
+fun viewModelModule() = module {
+ // Dashboard
+ viewModel { TodayViewModel() }
+ viewModel { SettingsViewModel() }
+ viewModel { AppInfoViewModel() }
+ // Form
+ viewModel { SetupNameViewModel() }
+ viewModel { SelectZoneViewModel() }
+ viewModel { SummaryViewModel() }
+}
+
+fun serviceModule() = module {
+ single { TwilightDatabase(get()) }
+}
+
+fun repositoryModule() = module {
+ single { KairosRepository() }
+ single { PreferencesRepositoryImpl() }
+ single { LocationRepositoryImpl() }
+ single { LocationFormRepositoryImpl() }
+}
+
+fun mapperModule() = module {
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/di/TwilightDatabaseWrapper.kt b/composeApp/src/commonMain/kotlin/di/TwilightDatabaseWrapper.kt
new file mode 100644
index 0000000..1c20d2f
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/di/TwilightDatabaseWrapper.kt
@@ -0,0 +1,5 @@
+package di
+
+import com.delacrixmorgan.twilight.TwilightDatabase
+
+class TwilightDatabaseWrapper(val instance: TwilightDatabase?)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/expect.kt b/composeApp/src/commonMain/kotlin/expect.kt
new file mode 100644
index 0000000..3417ba4
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/expect.kt
@@ -0,0 +1,21 @@
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import androidx.datastore.preferences.core.Preferences
+import okio.Path.Companion.toPath
+import org.koin.core.module.Module
+
+fun createDataStore(
+ producePath: () -> String,
+): DataStore = PreferenceDataStoreFactory.createWithPath(
+ corruptionHandler = null,
+ migrations = emptyList(),
+ produceFile = { producePath().toPath() },
+)
+
+expect fun platformModule(): Module
+
+expect fun randomUUID(): String
+
+expect fun getVersionCode(): String
+
+expect fun getVersionName(): String
diff --git a/composeApp/src/commonMain/kotlin/nav/NavHost.kt b/composeApp/src/commonMain/kotlin/nav/NavHost.kt
new file mode 100644
index 0000000..540a845
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/nav/NavHost.kt
@@ -0,0 +1,49 @@
+package nav
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import org.koin.compose.viewmodel.koinViewModel
+import ui.dashboard.DashboardScreen
+import ui.dashboard.settings.appinfo.AppInfoScreen
+import ui.dashboard.settings.appinfo.AppInfoViewModel
+import ui.form.name.SetupNameScreen
+import ui.form.name.SetupNameViewModel
+import ui.form.summary.SummaryScreen
+import ui.form.summary.SummaryViewModel
+import ui.form.zone.SelectZoneScreen
+import ui.form.zone.SelectZoneViewModel
+
+@Composable
+fun TwilightNavHost(navHostController: NavHostController = rememberNavController()) {
+ NavHost(
+ navController = navHostController,
+ startDestination = Routes.Dashboard
+ ) {
+ composable { DashboardScreen(navHostController) }
+ formGraph(navHostController)
+ }
+}
+
+fun NavGraphBuilder.formGraph(navHostController: NavHostController) {
+ composable {
+ val viewModel = koinViewModel()
+ SelectZoneScreen(state = viewModel.state.collectAsState().value, onAction = { viewModel.onAction(navHostController, it) })
+ }
+ composable {
+ val viewModel = koinViewModel()
+ SetupNameScreen(state = viewModel.state.collectAsState().value, onAction = { viewModel.onAction(navHostController, it) })
+ }
+ composable {
+ val viewModel = koinViewModel()
+ SummaryScreen(state = viewModel.state.collectAsState().value, onAction = { viewModel.onAction(navHostController, it) })
+ }
+ composable {
+ val viewModel = koinViewModel()
+ AppInfoScreen(state = viewModel.state, onAction = { viewModel.onAction(navHostController, it) })
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/nav/Routes.kt b/composeApp/src/commonMain/kotlin/nav/Routes.kt
new file mode 100644
index 0000000..bb1c41a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/nav/Routes.kt
@@ -0,0 +1,26 @@
+package nav
+
+import kotlinx.serialization.Serializable
+
+sealed class Routes {
+ @Serializable
+ data object Dashboard : Routes()
+
+ @Serializable
+ data object Today : Routes()
+
+ @Serializable
+ data object Settings : Routes()
+
+ @Serializable
+ data object FormSetupName : Routes()
+
+ @Serializable
+ data object FormSelectZone : Routes()
+
+ @Serializable
+ data object FormSummary : Routes()
+
+ @Serializable
+ data object AppInfo : Routes()
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/nav/dashboard/DashboardBottomNavHost.kt b/composeApp/src/commonMain/kotlin/nav/dashboard/DashboardBottomNavHost.kt
new file mode 100644
index 0000000..52ac82d
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/nav/dashboard/DashboardBottomNavHost.kt
@@ -0,0 +1,39 @@
+package nav.dashboard
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import nav.Routes
+import org.koin.compose.viewmodel.koinViewModel
+import ui.dashboard.settings.SettingsScreen
+import ui.dashboard.settings.SettingsViewModel
+import ui.dashboard.today.TodayScreen
+import ui.dashboard.today.TodayViewModel
+
+@Composable
+fun DashboardBottomNavHost(
+ navHostController: NavHostController,
+ bottomNavHostController: NavHostController,
+ innerPadding: PaddingValues,
+) {
+ NavHost(
+ modifier = Modifier.fillMaxSize(),
+ navController = bottomNavHostController,
+ startDestination = DashboardBottomNavItem.Today.route
+ ) {
+ composable {
+ val viewModel = koinViewModel()
+ TodayScreen(Modifier.padding(innerPadding), state = viewModel.state.collectAsState().value, onAction = { viewModel.onAction(navHostController, it) })
+ }
+ composable {
+ val viewModel = koinViewModel()
+ SettingsScreen(innerPadding, state = viewModel.state.collectAsState().value, onAction = { viewModel.onAction(navHostController, it) })
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/nav/dashboard/DashboardBottomNavItem.kt b/composeApp/src/commonMain/kotlin/nav/dashboard/DashboardBottomNavItem.kt
new file mode 100644
index 0000000..1de6331
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/nav/dashboard/DashboardBottomNavItem.kt
@@ -0,0 +1,24 @@
+package nav.dashboard
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Settings
+import androidx.compose.material.icons.rounded.Today
+import androidx.compose.ui.graphics.vector.ImageVector
+import nav.Routes
+
+enum class DashboardBottomNavItem(
+ val title: String,
+ val route: Routes,
+ val icon: ImageVector,
+) {
+ Today(
+ "Today",
+ Routes.Today,
+ Icons.Rounded.Today
+ ),
+ Settings(
+ "Settings",
+ Routes.Settings,
+ Icons.Rounded.Settings
+ ),
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/ComposeExtensions.kt b/composeApp/src/commonMain/kotlin/ui/ComposeExtensions.kt
new file mode 100644
index 0000000..4ca1b15
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/ComposeExtensions.kt
@@ -0,0 +1,14 @@
+package ui
+
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.ime
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.platform.LocalDensity
+
+@Composable
+fun keyboardShownState(): State {
+ val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
+ return rememberUpdatedState(isImeVisible)
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/component/IconButton.kt b/composeApp/src/commonMain/kotlin/ui/component/IconButton.kt
new file mode 100644
index 0000000..8f4eb00
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/IconButton.kt
@@ -0,0 +1,24 @@
+package ui.component
+
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+
+@Composable
+fun IconButton(
+ imageVector: ImageVector,
+ contentDescription: String,
+ tint: Color = MaterialTheme.colorScheme.onPrimaryContainer,
+ onClicked: () -> Unit,
+) {
+ IconButton(onClick = { onClicked() }) {
+ Icon(
+ imageVector = imageVector,
+ contentDescription = contentDescription,
+ tint = tint
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/component/ListItem.kt b/composeApp/src/commonMain/kotlin/ui/component/ListItem.kt
new file mode 100644
index 0000000..3bcfadb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/ListItem.kt
@@ -0,0 +1,55 @@
+package ui.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+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.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ListItem(
+ modifier: Modifier = Modifier,
+ startContent: @Composable (() -> Unit)? = null,
+ label: String? = null,
+ description: String? = null,
+ endContent: @Composable (() -> Unit)? = null,
+ labelColor: Color = MaterialTheme.colorScheme.onSurface,
+ descriptionColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
+ horizontalSpacedBy: Dp = 8.dp,
+ columnPadding: Dp = 4.dp,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth().then(modifier),
+ horizontalArrangement = Arrangement.spacedBy(horizontalSpacedBy),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ startContent?.invoke()
+ Column(
+ modifier = Modifier.weight(1F),
+ verticalArrangement = Arrangement.spacedBy(columnPadding),
+ ) {
+ if (!label.isNullOrBlank()) {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge,
+ color = labelColor,
+ )
+ }
+ if (!description.isNullOrBlank()) {
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = descriptionColor,
+ )
+ }
+ }
+ endContent?.invoke()
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/component/ListItemRow.kt b/composeApp/src/commonMain/kotlin/ui/component/ListItemRow.kt
new file mode 100644
index 0000000..5abdaa1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/ListItemRow.kt
@@ -0,0 +1,72 @@
+package ui.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.CheckCircle
+import androidx.compose.material.icons.rounded.RadioButtonUnchecked
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+
+@Composable
+internal fun ListItemRow(
+ label: String,
+ description: String,
+ endLabel: String? = null,
+ selected: Boolean = false,
+ onClicked: (() -> Unit)? = null
+) {
+ Box(
+ modifier = Modifier
+ .clip(MaterialTheme.shapes.large)
+ .background(MaterialTheme.colorScheme.surfaceContainerLow, shape = MaterialTheme.shapes.large)
+ .then(if (onClicked != null) Modifier.clickable { onClicked() } else Modifier)
+ .padding(8.dp)
+ ) {
+ ListItem(
+ modifier = Modifier.padding(vertical = 8.dp),
+ startContent = {
+ if (onClicked != null) {
+ if (selected) {
+ Icon(
+ Icons.Rounded.CheckCircle,
+ modifier = Modifier.padding(16.dp),
+ contentDescription = null
+ )
+ } else {
+ Icon(
+ Icons.Rounded.RadioButtonUnchecked,
+ modifier = Modifier.padding(16.dp),
+ contentDescription = null
+ )
+ }
+ } else {
+ Spacer(Modifier.width(16.dp))
+ }
+ },
+ label = label,
+ description = description,
+ endContent = {
+ if (!endLabel.isNullOrBlank()) {
+ Text(
+ text = endLabel,
+ modifier = Modifier.padding(16.dp),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ } else {
+ Spacer(Modifier.width(16.dp))
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/component/ListView.kt b/composeApp/src/commonMain/kotlin/ui/component/ListView.kt
new file mode 100644
index 0000000..936274a
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/ListView.kt
@@ -0,0 +1,20 @@
+package ui.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun ListView(
+ modifier: Modifier = Modifier,
+ data: List<@Composable (ColumnScope.() -> Unit)>,
+ divider: @Composable (() -> Unit)? = null,
+) {
+ Column(modifier) {
+ data.forEachIndexed { index, listView ->
+ if (index != 0) divider?.invoke()
+ listView()
+ }
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/ui/component/RadioGroup.kt b/composeApp/src/commonMain/kotlin/ui/component/RadioGroup.kt
new file mode 100644
index 0000000..0b417d7
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/RadioGroup.kt
@@ -0,0 +1,88 @@
+package ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.unit.dp
+
+@Composable
+internal fun RadioGroup(
+ modifier: Modifier = Modifier,
+ haptic: HapticFeedback = LocalHapticFeedback.current,
+ initialIndex: Int? = null,
+ options: List,
+ onSelected: (Int, String) -> Unit,
+) {
+ var selectedIndex by remember(initialIndex) { mutableStateOf(initialIndex) }
+ Column(modifier = modifier) {
+ options.forEachIndexed { index, option ->
+ val isSelected = index == selectedIndex
+ val selected = {
+ selectedIndex = index
+ onSelected(index, option.id)
+ haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+ }
+ RadioRow(
+ isSelected = isSelected,
+ option = option,
+ onSelected = selected,
+ )
+ }
+ }
+}
+
+@Composable
+private fun RadioRow(
+ isSelected: Boolean,
+ option: RadioRowData,
+ onSelected: () -> Unit,
+) {
+ Row(
+ modifier = Modifier.clickable { onSelected() }.padding(vertical = 8.dp, horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = isSelected,
+ onClick = onSelected,
+ )
+
+ Column(
+ modifier = Modifier.weight(1F),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = option.label,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ if (!option.description.isNullOrBlank()) {
+ Text(
+ text = option.description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+ }
+}
+
+data class RadioRowData(
+ val id: String = "",
+ val label: String,
+ val description: String? = null,
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/component/RadioGroupBottomSheet.kt b/composeApp/src/commonMain/kotlin/ui/component/RadioGroupBottomSheet.kt
new file mode 100644
index 0000000..62dad7c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/RadioGroupBottomSheet.kt
@@ -0,0 +1,66 @@
+package ui.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import ui.theme.AppTypography
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun RadioGroupBottomSheet(
+ title: String,
+ dismissLabel: String = "Done",
+ selectedIndex: Int,
+ items: List,
+ isVisible: Boolean,
+ onSelected: (RadioRowData) -> Unit,
+ onDismissed: () -> Unit
+) {
+ if (!isVisible) return
+ val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
+ ModalBottomSheet(
+ onDismissRequest = onDismissed,
+ sheetState = sheetState,
+ windowInsets = WindowInsets(0, 0, 0, 0),
+ ) {
+ Column(
+ modifier = Modifier.padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ title,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = AppTypography.titleLarge
+ )
+ RadioGroup(
+ initialIndex = selectedIndex,
+ options = items,
+ onSelected = { index, _ ->
+ onSelected(items[index])
+ }
+ )
+
+ Button(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ onClick = { onDismissed() },
+ ) {
+ Text(dismissLabel, modifier = Modifier.padding(vertical = 8.dp))
+ }
+ Spacer(Modifier.height(16.dp))
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/component/VerticalScrollWheel.kt b/composeApp/src/commonMain/kotlin/ui/component/VerticalScrollWheel.kt
new file mode 100644
index 0000000..4e62a32
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/VerticalScrollWheel.kt
@@ -0,0 +1,79 @@
+package ui.component
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.distinctUntilChanged
+import ui.dashboard.today.TodayViewModel.Companion.SCROLL_WHEEL_PAGE_SIZE
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun VerticalScrollWheel(
+ modifier: Modifier,
+ listState: LazyListState,
+ onScrolled: (Int, Boolean) -> Unit,
+ buffer: Int = 2,
+) {
+ val items = remember { mutableStateListOf() }
+
+ LazyColumn(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ horizontalAlignment = Alignment.End,
+ state = listState,
+ flingBehavior = rememberSnapFlingBehavior(lazyListState = listState)
+ ) {
+ items(items, key = { it }) { index ->
+ val width = if (index % 3 == 0) 32.dp else 16.dp
+ HorizontalDivider(
+ Modifier.width(width),
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ }
+
+ val listStateListener: State = remember {
+ derivedStateOf {
+ val layoutInfo = listState.layoutInfo
+ val totalItemsNumber = layoutInfo.totalItemsCount
+ val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
+
+ InfiniteListContent(
+ firstVisibleIndex = listState.firstVisibleItemIndex,
+ isFirstItemVisible = listState.firstVisibleItemIndex == 0,
+ reachedBottom = lastVisibleItemIndex > (totalItemsNumber - buffer)
+ )
+ }
+ }
+
+ LaunchedEffect(listState) {
+ snapshotFlow { listStateListener.value }
+ .distinctUntilChanged()
+ .collect { content ->
+ onScrolled(content.firstVisibleIndex, content.isFirstItemVisible)
+ if (content.reachedBottom) items.addAll((items.size..items.size + SCROLL_WHEEL_PAGE_SIZE))
+ }
+ }
+}
+
+private data class InfiniteListContent(
+ val firstVisibleIndex: Int,
+ val isFirstItemVisible: Boolean,
+ val reachedBottom: Boolean,
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/component/navigationIcon/NavigationBackIcon.kt b/composeApp/src/commonMain/kotlin/ui/component/navigationIcon/NavigationBackIcon.kt
new file mode 100644
index 0000000..fe6530b
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/component/navigationIcon/NavigationBackIcon.kt
@@ -0,0 +1,23 @@
+package ui.component.navigationIcon
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+@Composable
+fun NavigationBackIcon(
+ tint: Color = MaterialTheme.colorScheme.onPrimaryContainer,
+ onClick: () -> Unit,
+) {
+ IconButton(onClick = onClick) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = "Go back",
+ tint = tint
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/DashboardScreen.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/DashboardScreen.kt
new file mode 100644
index 0000000..e995c0c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/DashboardScreen.kt
@@ -0,0 +1,54 @@
+package ui.dashboard
+
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import nav.dashboard.DashboardBottomNavHost
+import nav.dashboard.DashboardBottomNavItem
+
+@Composable
+fun DashboardScreen(
+ navHostController: NavHostController,
+ bottomNavHostController: NavHostController = rememberNavController(),
+) {
+ Scaffold(
+ bottomBar = { BottomNavigationBar(bottomNavHostController) }
+ ) { innerPadding ->
+ DashboardBottomNavHost(navHostController, bottomNavHostController, innerPadding)
+ }
+}
+
+@Composable
+private fun BottomNavigationBar(navHostController: NavHostController) {
+ val navBackStackEntry by navHostController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route
+
+ NavigationBar {
+ DashboardBottomNavItem.entries.forEach { navItem ->
+ val selected by remember(currentRoute) {
+ derivedStateOf { currentRoute == navItem.route::class.qualifiedName }
+ }
+ NavigationBarItem(
+ icon = { Icon(navItem.icon, contentDescription = navItem.title) },
+ selected = selected,
+ onClick = {
+ if (selected) return@NavigationBarItem
+ navHostController.navigate(navItem.route) {
+ popUpTo(navHostController.graph.findStartDestination().id) { saveState = true }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsAppSection.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsAppSection.kt
new file mode 100644
index 0000000..a45ac06
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsAppSection.kt
@@ -0,0 +1,107 @@
+package ui.dashboard.settings
+
+import androidx.compose.foundation.clickable
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ChevronRight
+import androidx.compose.material.icons.rounded.Feedback
+import androidx.compose.material.icons.rounded.Info
+import androidx.compose.material.icons.rounded.Policy
+import androidx.compose.material.icons.rounded.ThumbUp
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import ui.component.ListItem
+import ui.theme.TwilightModifiers
+
+@Composable
+internal fun AppInfo(onClick: () -> Unit) {
+ val label = "App Info"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.Info,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
+
+@Composable
+internal fun PrivacyPolicy(onClick: () -> Unit) {
+ val label = "Privacy Policy"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.Policy,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
+
+@Composable
+internal fun SendFeedback(onClick: () -> Unit) {
+ val label = "Send Feedback"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.Feedback,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
+
+@Composable
+internal fun RateUs(onClick: () -> Unit) {
+ val label = "Rate Us"
+
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ThumbUp,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsGeneralSection.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsGeneralSection.kt
new file mode 100644
index 0000000..84a2fc7
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsGeneralSection.kt
@@ -0,0 +1,82 @@
+package ui.dashboard.settings
+
+import androidx.compose.foundation.clickable
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Badge
+import androidx.compose.material.icons.rounded.ChevronRight
+import androidx.compose.material.icons.rounded.DateRange
+import androidx.compose.material.icons.rounded.FormatPaint
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import ui.component.ListItem
+import ui.theme.TwilightModifiers
+
+@Composable
+internal fun Theme(onClick: () -> Unit) {
+ val label = "Theme"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.FormatPaint,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
+
+@Composable
+internal fun DateFormat(onClick: () -> Unit) {
+ val label = "Date Format"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.DateRange,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
+
+@Composable
+internal fun LocationFormat(onClick: () -> Unit) {
+ val label = "Location Format"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.Badge,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsScreen.kt
new file mode 100644
index 0000000..cdc25ae
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsScreen.kt
@@ -0,0 +1,152 @@
+package ui.dashboard.settings
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.platform.UriHandler
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import data.preferences.model.DateFormatPreference
+import data.preferences.model.LocationFormatPreference
+import data.preferences.model.ThemePreference
+import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.resources.stringResource
+import twilight.composeapp.generated.resources.Res
+import twilight.composeapp.generated.resources.app_name
+import twilight.composeapp.generated.resources.ic_logo_foreground
+import ui.component.ListView
+import ui.component.RadioGroupBottomSheet
+import ui.component.RadioRowData
+import ui.theme.AppTypography
+
+@Composable
+fun SettingsScreen(
+ innerPadding: PaddingValues,
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ uriHandler: UriHandler = LocalUriHandler.current,
+ state: SettingsUiState,
+ onAction: (SettingsAction) -> Unit,
+) {
+ Column(
+ Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ .padding(horizontal = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(Modifier.height(32.dp))
+ Text("Settings", style = AppTypography.headlineMedium)
+ Spacer(Modifier.height(24.dp))
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ ) {
+ ListView(
+ data = listOf(
+ { Theme { onAction(SettingsAction.ToggleThemeVisibility(show = true)) } },
+ { DateFormat { onAction(SettingsAction.ToggleDateFormatVisibility(show = true)) } },
+ { LocationFormat { onAction(SettingsAction.ToggleLocationFormatVisibility(show = true)) } },
+ ),
+ divider = { HorizontalDivider() }
+ )
+ }
+ Spacer(Modifier.height(16.dp))
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ ) {
+ ListView(
+ data = listOf(
+ { AppInfo { onAction(SettingsAction.OpenAppInfo) } },
+ { PrivacyPolicy { onAction(SettingsAction.OpenPrivacyPolicy(open = true)) } },
+ { SendFeedback { onAction(SettingsAction.OpenSendFeedback(open = true)) } },
+ { RateUs { onAction(SettingsAction.OpenRateUs(open = true)) } },
+ ),
+ divider = { HorizontalDivider() }
+ )
+ }
+ Spacer(Modifier.height(32.dp))
+
+ Image(painter = painterResource(Res.drawable.ic_logo_foreground), "Twilight Logo")
+
+ Spacer(Modifier.height(8.dp))
+
+ Text(
+ stringResource(Res.string.app_name) + " " + state.version,
+ style = MaterialTheme.typography.labelLarge
+ )
+ }
+
+ RadioGroupBottomSheet(
+ title = "Theme",
+ selectedIndex = state.theme.ordinal,
+ items = ThemePreference.entries.map { RadioRowData(id = it.name, label = it.label) },
+ isVisible = state.showTheme,
+ onSelected = { selectedItem ->
+ onAction(SettingsAction.OnThemeSelected(ThemePreference.entries.first { it.name == selectedItem.id }))
+ },
+ onDismissed = { onAction(SettingsAction.ToggleThemeVisibility(show = false)) }
+ )
+
+ RadioGroupBottomSheet(
+ title = "Date Format",
+ selectedIndex = state.dateFormat.ordinal,
+ items = DateFormatPreference.entries.map { RadioRowData(id = it.name, label = it.label, description = it.description) },
+ isVisible = state.showDateFormat,
+ onSelected = { selectedItem ->
+ onAction(SettingsAction.OnDateFormatSelected(DateFormatPreference.entries.first { it.name == selectedItem.id }))
+ },
+ onDismissed = { onAction(SettingsAction.ToggleDateFormatVisibility(show = false)) }
+ )
+
+ RadioGroupBottomSheet(
+ title = "Location Format",
+ selectedIndex = state.locationFormat.ordinal,
+ items = LocationFormatPreference.entries.map { RadioRowData(id = it.name, label = it.label, description = it.description) },
+ isVisible = state.showLocationFormat,
+ onSelected = { selectedItem ->
+ onAction(SettingsAction.OnLocationFormatSelected(LocationFormatPreference.entries.first { it.name == selectedItem.id }))
+ },
+ onDismissed = { onAction(SettingsAction.ToggleLocationFormatVisibility(show = false)) }
+ )
+
+ LaunchedEffect(state, lifecycleOwner) {
+ lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
+ if (state.openPrivacyPolicy) {
+ uriHandler.openUri("https://github.com/delacrixmorgan/twilight-kmp/blob/main/PRIVACY_POLICY.md")
+ onAction(SettingsAction.OpenPrivacyPolicy(open = false))
+ }
+ if (state.openSendFeedback) {
+ val email = "delacrixmorgan@gmail.com"
+ val subject = "Twilight - App Feedback"
+ uriHandler.openUri("mailto:$email?subject=$subject")
+ onAction(SettingsAction.OpenSendFeedback(open = false))
+ }
+ if (state.openRateUs) {
+ uriHandler.openUri("https://play.google.com/store/apps/details?id=com.delacrixmorgan.twilight")
+ onAction(SettingsAction.OpenRateUs(open = false))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsViewModel.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsViewModel.kt
new file mode 100644
index 0000000..5a5b662
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/SettingsViewModel.kt
@@ -0,0 +1,105 @@
+package ui.dashboard.settings
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
+import data.preferences.PreferencesRepository
+import data.preferences.model.DateFormatPreference
+import data.preferences.model.LocationFormatPreference
+import data.preferences.model.ThemePreference
+import getVersionCode
+import getVersionName
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import nav.Routes
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class SettingsViewModel : ViewModel(), KoinComponent {
+ private var _state = MutableStateFlow(SettingsUiState())
+ val state: StateFlow
+ get() = _state.asStateFlow()
+
+ private val preferences: PreferencesRepository by inject()
+
+ init {
+ _state.update { it.copy(version = "${getVersionName()} (${getVersionCode()})") }
+ loadPreferences()
+ }
+
+ private fun loadPreferences() {
+ viewModelScope.launch {
+ launch { preferences.getTheme().collect { theme -> _state.update { it.copy(theme = theme) } } }
+ launch { preferences.getDateFormat().collect { dateFormat -> _state.update { it.copy(dateFormat = dateFormat) } } }
+ launch { preferences.getLocationFormat().collect { locationFormat -> _state.update { it.copy(locationFormat = locationFormat) } } }
+ }
+ }
+
+ fun onAction(navHostController: NavHostController, action: SettingsAction) {
+ when (action) {
+ is SettingsAction.ToggleThemeVisibility -> {
+ _state.update { it.copy(showTheme = action.show) }
+ }
+ is SettingsAction.ToggleDateFormatVisibility -> {
+ _state.update { it.copy(showDateFormat = action.show) }
+ }
+ is SettingsAction.ToggleLocationFormatVisibility -> {
+ _state.update { it.copy(showLocationFormat = action.show) }
+ }
+ is SettingsAction.OpenAppInfo -> {
+ navHostController.navigate(Routes.AppInfo)
+ }
+ is SettingsAction.OpenPrivacyPolicy -> {
+ _state.update { it.copy(openPrivacyPolicy = action.open) }
+ }
+ is SettingsAction.OpenRateUs -> {
+ _state.update { it.copy(openRateUs = action.open) }
+ }
+ is SettingsAction.OpenSendFeedback -> {
+ _state.update { it.copy(openSendFeedback = action.open) }
+ }
+ is SettingsAction.OnThemeSelected -> {
+ viewModelScope.launch { preferences.saveTheme(action.theme) }
+ }
+ is SettingsAction.OnDateFormatSelected -> {
+ viewModelScope.launch { preferences.saveDateFormat(action.dateFormat) }
+ }
+ is SettingsAction.OnLocationFormatSelected -> {
+ viewModelScope.launch { preferences.saveLocationFormat(action.locationFormat) }
+ }
+ }
+ }
+}
+
+data class SettingsUiState(
+ val version: String = "",
+ val theme: ThemePreference = ThemePreference.Default,
+ val dateFormat: DateFormatPreference = DateFormatPreference.Default,
+ val locationFormat: LocationFormatPreference = LocationFormatPreference.Default,
+
+ val showTheme: Boolean = false,
+ val showDateFormat: Boolean = false,
+ val showLocationFormat: Boolean = false,
+
+ val openPrivacyPolicy: Boolean = false,
+ val openSendFeedback: Boolean = false,
+ val openRateUs: Boolean = false
+)
+
+sealed interface SettingsAction {
+ data class ToggleThemeVisibility(val show: Boolean) : SettingsAction
+ data class ToggleDateFormatVisibility(val show: Boolean) : SettingsAction
+ data class ToggleLocationFormatVisibility(val show: Boolean) : SettingsAction
+
+ data object OpenAppInfo : SettingsAction
+ data class OpenPrivacyPolicy(val open: Boolean) : SettingsAction
+ data class OpenSendFeedback(val open: Boolean) : SettingsAction
+ data class OpenRateUs(val open: Boolean) : SettingsAction
+
+ data class OnThemeSelected(val theme: ThemePreference) : SettingsAction
+ data class OnDateFormatSelected(val dateFormat: DateFormatPreference) : SettingsAction
+ data class OnLocationFormatSelected(val locationFormat: LocationFormatPreference) : SettingsAction
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/settings/appinfo/AppInfoScreen.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/appinfo/AppInfoScreen.kt
new file mode 100644
index 0000000..1d08c51
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/appinfo/AppInfoScreen.kt
@@ -0,0 +1,143 @@
+package ui.dashboard.settings.appinfo
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ChevronRight
+import androidx.compose.material.icons.rounded.Code
+import androidx.compose.material.icons.rounded.Person
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.platform.UriHandler
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.repeatOnLifecycle
+import org.jetbrains.compose.ui.tooling.preview.Preview
+import ui.component.ListItem
+import ui.component.ListView
+import ui.component.navigationIcon.NavigationBackIcon
+import ui.theme.AppTheme
+import ui.theme.AppTypography
+import ui.theme.TwilightModifiers
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AppInfoScreen(
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ uriHandler: UriHandler = LocalUriHandler.current,
+ state: AppInfoUiState,
+ onAction: (AppInfoAction) -> Unit,
+) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("App Info", style = AppTypography.headlineMedium) },
+ navigationIcon = { NavigationBackIcon(tint = MaterialTheme.colorScheme.onSurface) { onAction(AppInfoAction.GoBack) } },
+ )
+ },
+ ) { innerPadding ->
+ Column(Modifier.fillMaxSize().padding(innerPadding).padding(16.dp)) {
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ ) {
+ ListView(
+ data = listOf(
+ { Developer { onAction(AppInfoAction.OpenDeveloper(show = true)) } },
+ { SourceCode { onAction(AppInfoAction.OpenSourceCode(show = true)) } },
+ ),
+ divider = { HorizontalDivider() }
+ )
+ }
+ }
+ }
+
+ LaunchedEffect(state, lifecycleOwner) {
+ lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.STARTED) {
+ if (state.openDeveloper) {
+ uriHandler.openUri("https://github.com/delacrixmorgan")
+ onAction(AppInfoAction.OpenDeveloper(show = false))
+ }
+ if (state.openSourceCode) {
+ uriHandler.openUri("https://github.com/delacrixmorgan/twilight-kmp")
+ onAction(AppInfoAction.OpenSourceCode(show = false))
+ }
+ }
+ }
+}
+
+@Composable
+private fun Developer(onClick: () -> Unit) {
+ val label = "Developer"
+ val description = "Delacrix Morgan"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ description = description,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.Person,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
+
+@Composable
+private fun SourceCode(onClick: () -> Unit) {
+ val label = "Source Code"
+ val description = "GitHub"
+ ListItem(
+ modifier = Modifier.clickable { onClick() },
+ label = label,
+ description = description,
+ startContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.Code,
+ contentDescription = label
+ )
+ },
+ endContent = {
+ Icon(
+ modifier = TwilightModifiers.iconModifier,
+ imageVector = Icons.Rounded.ChevronRight,
+ contentDescription = label
+ )
+ }
+ )
+}
+
+@Preview
+@Composable
+private fun AppInfoScreenPreview() {
+ AppTheme {
+ AppInfoScreen(state = AppInfoUiState(), onAction = {})
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/settings/appinfo/AppInfoViewModel.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/appinfo/AppInfoViewModel.kt
new file mode 100644
index 0000000..65ed6a1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/settings/appinfo/AppInfoViewModel.kt
@@ -0,0 +1,33 @@
+package ui.dashboard.settings.appinfo
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.navigation.NavHostController
+import org.koin.core.component.KoinComponent
+
+class AppInfoViewModel : ViewModel(), KoinComponent {
+ var state by mutableStateOf(AppInfoUiState())
+ private set
+
+ fun onAction(navHostController: NavHostController, action: AppInfoAction) {
+ when (action) {
+ is AppInfoAction.OpenDeveloper -> state = state.copy(openDeveloper = action.show)
+ is AppInfoAction.OpenSourceCode -> state = state.copy(openSourceCode = action.show)
+ AppInfoAction.GoBack -> navHostController.navigateUp()
+ }
+ }
+}
+
+data class AppInfoUiState(
+ val openDeveloper: Boolean = false,
+ val openSourceCode: Boolean = false,
+)
+
+sealed interface AppInfoAction {
+ data class OpenDeveloper(val show: Boolean) : AppInfoAction
+ data class OpenSourceCode(val show: Boolean) : AppInfoAction
+
+ data object GoBack : AppInfoAction
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/today/TodayScreen.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/today/TodayScreen.kt
new file mode 100644
index 0000000..b8df932
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/today/TodayScreen.kt
@@ -0,0 +1,270 @@
+package ui.dashboard.today
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+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.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.ArrowUpward
+import androidx.compose.material.icons.rounded.Edit
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import data.location.model.Location
+import data.preferences.model.DateFormatPreference
+import data.preferences.model.LocationFormatPreference
+import data.utils.DateFormat
+import data.utils.now
+import kotlinx.coroutines.launch
+import kotlinx.datetime.DateTimePeriod
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.format
+import kotlinx.datetime.offsetIn
+import kotlinx.datetime.plus
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
+import ui.component.VerticalScrollWheel
+import ui.dashboard.today.TodayViewModel.Companion.SCROLL_WHEEL_PAGE_SIZE
+import ui.dashboard.today.TodayViewModel.Companion.SMOOTH_SCROLL_IN_MINUTES_THRESHOLD
+
+@Composable
+fun TodayScreen(
+ modifier: Modifier,
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ state: TodayUiState,
+ onAction: (TodayAction) -> Unit,
+) {
+ val listState = rememberLazyListState()
+ val coroutineScope = rememberCoroutineScope()
+
+ Box(modifier = modifier.fillMaxSize()) {
+ Row {
+ Column(Modifier.weight(2F).padding(top = 16.dp)) {
+ state.localLocation?.let { location ->
+ NameTimeView(
+ locationFormatPreference = state.locationFormatPreference,
+ dateFormatPreference = state.dateFormatPreference,
+ offsetInMinutes = state.offsetInMinutes,
+ location = location
+ )
+ HorizontalDivider(color = MaterialTheme.colorScheme.onSurface)
+ }
+
+ LazyColumn(state = rememberLazyListState()) {
+ items(count = state.locations.size, key = { state.locations[it].id }) { index ->
+ val location = state.locations[index]
+ EditableNameTimeView(
+ locationFormatPreference = state.locationFormatPreference,
+ dateFormatPreference = state.dateFormatPreference,
+ offsetInMinutes = state.offsetInMinutes,
+ location = location,
+ onClick = { onAction(TodayAction.OnSelectedLocation(it)) },
+ )
+ }
+ }
+ Spacer(Modifier.height(32.dp))
+ }
+
+ VerticalScrollWheel(
+ modifier = Modifier.weight(1F).fillMaxSize().defaultMinSize(minHeight = 0.dp).padding(horizontal = 24.dp),
+ listState = listState,
+ onScrolled = { offsetInMinutes, isFirstItemVisible ->
+ onAction(TodayAction.OnTimeWheelScrolled(offsetInMinutes, isFirstItemVisible))
+ }
+ )
+ }
+
+ Box(modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)) {
+ FloatingActionButton(onClick = { onAction(TodayAction.OpenCreateLocation) }) {
+ Icon(Icons.Rounded.Add, "Add")
+ }
+ }
+
+ Box(modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 16.dp)) {
+ AnimatedVisibility(
+ visible = !state.isFirstItemVisible && state.offsetInMinutes != 0,
+ enter = slideInVertically(
+ initialOffsetY = { it / 2 },
+ ),
+ exit = slideOutVertically(
+ targetOffsetY = { it / 2 },
+ ),
+ ) {
+ Button(
+ contentPadding = PaddingValues(
+ start = 16.dp,
+ top = 4.dp,
+ end = 12.dp,
+ bottom = 4.dp,
+ ),
+ onClick = { onAction(TodayAction.OnScrollToTopClicked(scroll = true)) }
+ ) {
+ Text(state.formattedOffSetInMinutes)
+ Spacer(Modifier.width(4.dp))
+ Icon(Icons.Rounded.ArrowUpward, "Up")
+ }
+ }
+ }
+ }
+
+ EditLocationDialog(
+ isVisible = state.openEditLocationDialog,
+ location = state.selectedLocation,
+ locationFormatPreference = state.locationFormatPreference,
+ onDelete = { onAction(TodayAction.OnItemDeleteClicked) },
+ onEdit = { onAction(TodayAction.OnItemEditClicked) },
+ onDismiss = { onAction(TodayAction.CloseEditLocationDialog) }
+ )
+
+ LaunchedEffect(state, lifecycleOwner) {
+ if (state.scrollToTop) {
+ coroutineScope.launch {
+ if (state.offsetInMinutes > SMOOTH_SCROLL_IN_MINUTES_THRESHOLD) {
+ listState.scrollToItem(SCROLL_WHEEL_PAGE_SIZE)
+ listState.animateScrollToItem(0)
+ } else {
+ listState.animateScrollToItem(0)
+ }
+ onAction(TodayAction.OnScrollToTopClicked(scroll = false))
+ }
+ }
+ }
+}
+
+@Composable
+internal fun EditLocationDialog(
+ isVisible: Boolean,
+ location: Location?,
+ locationFormatPreference: LocationFormatPreference,
+ onDelete: () -> Unit,
+ onEdit: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ if (!isVisible) return
+ val label = when (locationFormatPreference) {
+ LocationFormatPreference.Place,
+ LocationFormatPreference.PlaceWithGMT -> location?.regionName
+ LocationFormatPreference.Person,
+ LocationFormatPreference.PersonWithGMT -> location?.name
+ }
+ AlertDialog(
+ icon = { Icon(Icons.Rounded.Edit, contentDescription = "Edit") },
+ title = { Text(text = "Change Location") },
+ text = { Text(text = "What would you like to do to location \"$label\"?") },
+ confirmButton = {
+ TextButton(onClick = { onDelete() }) { Text("Delete", color = MaterialTheme.colorScheme.error) }
+ },
+ dismissButton = {
+ TextButton(onClick = { onEdit() }) { Text("Edit") }
+ },
+ onDismissRequest = { onDismiss() },
+ )
+}
+
+@Composable
+private fun EditableNameTimeView(
+ locationFormatPreference: LocationFormatPreference,
+ dateFormatPreference: DateFormatPreference,
+ offsetInMinutes: Int,
+ location: Location,
+ onClick: ((Location) -> Unit)
+) {
+ NameTimeView(
+ locationFormatPreference = locationFormatPreference,
+ dateFormatPreference = dateFormatPreference,
+ offsetInMinutes = offsetInMinutes,
+ location = location,
+ onClick = onClick
+ )
+}
+
+@Composable
+private fun NameTimeView(
+ locationFormatPreference: LocationFormatPreference,
+ dateFormatPreference: DateFormatPreference,
+ offsetInMinutes: Int,
+ location: Location,
+ onClick: ((Location) -> Unit)? = null,
+) {
+ val offsetMinutes = DateTimePeriod(minutes = offsetInMinutes)
+ val adjustedTime = LocalDateTime.now(location.zone).toInstant(location.zone).plus(offsetMinutes, TimeZone.UTC).toLocalDateTime(location.zone)
+ val hourMinuteTime = adjustedTime.format(
+ when (dateFormatPreference) {
+ DateFormatPreference.Twelve -> DateFormat.twelveHour
+ DateFormatPreference.TwentyFour -> DateFormat.twentyFourHour
+ }
+ )
+ val dateMonthTime = adjustedTime.format(DateFormat.dayOfWeekDayMonth)
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.background)
+ .then(if (onClick != null) Modifier.clickable { onClick(location) } else Modifier)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = when (locationFormatPreference) {
+ LocationFormatPreference.Place,
+ LocationFormatPreference.PlaceWithGMT -> location.regionName
+ LocationFormatPreference.Person,
+ LocationFormatPreference.PersonWithGMT -> location.name
+ },
+ style = MaterialTheme.typography.titleLarge
+ )
+ if (locationFormatPreference == LocationFormatPreference.PlaceWithGMT || locationFormatPreference == LocationFormatPreference.PersonWithGMT) {
+ val gmtOffset = adjustedTime.toInstant(location.zone).offsetIn(location.zone).toString()
+ Text(
+ modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer, shape = RoundedCornerShape(8.dp)).padding(6.dp),
+ color = MaterialTheme.colorScheme.onSurface,
+ text = "GMT $gmtOffset",
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+ Text(
+ text = dateMonthTime,
+ style = MaterialTheme.typography.bodyLarge
+ )
+
+ Text(
+ text = hourMinuteTime,
+ style = MaterialTheme.typography.displayMedium
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/dashboard/today/TodayViewModel.kt b/composeApp/src/commonMain/kotlin/ui/dashboard/today/TodayViewModel.kt
new file mode 100644
index 0000000..63db639
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/dashboard/today/TodayViewModel.kt
@@ -0,0 +1,193 @@
+package ui.dashboard.today
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
+import data.location.LocationRepository
+import data.locationform.LocationFormRepository
+import data.location.model.Location
+import data.preferences.model.DateFormatPreference
+import data.preferences.model.LocationFormatPreference
+import data.preferences.PreferencesRepository
+import data.kairos.KairosRepository
+import data.utils.now
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toInstant
+import nav.Routes
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class TodayViewModel : ViewModel(), KoinComponent {
+ companion object {
+ const val SCROLL_WHEEL_PAGE_SIZE = 300
+ const val SMOOTH_SCROLL_IN_MINUTES_THRESHOLD = 5 * 60
+ }
+
+ private val preferences: PreferencesRepository by inject()
+ private val repository: LocationRepository by inject()
+ private val kairosRepository: KairosRepository by inject()
+ private val locationFormRepository: LocationFormRepository by inject()
+
+ private var _state = MutableStateFlow(TodayUiState())
+ val state: StateFlow
+ get() = _state.asStateFlow()
+
+ init {
+ loadPreferences()
+ loadLocations()
+ }
+
+ private fun loadPreferences() {
+ viewModelScope.launch {
+ launch {
+ preferences.getDateFormat().collect { dateFormat ->
+ _state.update { it.copy(dateFormatPreference = dateFormat) }
+ }
+ }
+ launch {
+ preferences.getLocationFormat().collect { locationFormat ->
+ _state.update { it.copy(locationFormatPreference = locationFormat) }
+ }
+ }
+ }
+ }
+
+ private fun loadLocations() {
+ val currentTimeZone = TimeZone.currentSystemDefault()
+ val zone = kairosRepository.search(currentTimeZone.id)
+ if (zone != null) {
+ _state.update {
+ it.copy(
+ localLocation = Location(
+ name = zone.city,
+ regionName = zone.city,
+ zoneId = zone.zoneIdString
+ )
+ )
+ }
+ }
+ viewModelScope.launch {
+ repository.getLocations().collect { locations ->
+ _state.update {
+ it.copy(
+ locations = locations.sortedBy { location ->
+ LocalDateTime.now(location.zone).toInstant(TimeZone.UTC).epochSeconds
+ }
+ )
+ }
+ }
+ }
+ }
+
+ fun onAction(navHostController: NavHostController, action: TodayAction) {
+ when (action) {
+ TodayAction.OpenCreateLocation -> {
+ viewModelScope.launch {
+ locationFormRepository.clear()
+ navHostController.navigate(Routes.FormSelectZone)
+ }
+ }
+ TodayAction.CloseEditLocationDialog -> {
+ _state.update { it.copy(openEditLocationDialog = false) }
+
+ }
+ TodayAction.OnItemEditClicked -> {
+ viewModelScope.launch {
+ state.value.selectedLocation?.let {
+ locationFormRepository.saveID(it.id)
+ locationFormRepository.saveName(it.name)
+ locationFormRepository.saveRegionName(it.regionName)
+ locationFormRepository.saveZoneId(it.zoneId)
+ }
+ _state.update {
+ it.copy(
+ openEditLocation = true,
+ openEditLocationDialog = false
+ )
+ }
+ navHostController.navigate(Routes.FormSelectZone)
+ }
+ }
+ TodayAction.OnItemDeleteClicked -> {
+ viewModelScope.launch {
+ state.value.selectedLocation?.id?.let { locationId ->
+ repository.deleteLocation(locationId)
+ _state.update {
+ it.copy(
+ selectedLocation = null,
+ openEditLocationDialog = false
+ )
+ }
+ }
+ }
+ }
+ is TodayAction.OnSelectedLocation -> {
+ _state.update {
+ it.copy(
+ selectedLocation = action.location,
+ openEditLocationDialog = true
+ )
+ }
+ }
+ is TodayAction.OnScrollToTopClicked -> {
+ _state.update { it.copy(scrollToTop = action.scroll) }
+ }
+ is TodayAction.OnTimeWheelScrolled -> {
+ _state.update {
+ it.copy(
+ offsetInMinutes = action.offsetInMinutes,
+ formattedOffSetInMinutes = "+ ${formatOffSetInMinutes(action.offsetInMinutes)}",
+ isFirstItemVisible = state.value.isFirstItemVisible,
+ )
+ }
+ }
+ }
+ }
+
+ private fun formatOffSetInMinutes(offSetMinutes: Int): String {
+ val days = offSetMinutes / (24 * 60)
+ val hours = (offSetMinutes % (24 * 60)) / 60
+ val minutes = offSetMinutes % 60
+
+ return buildString {
+ if (days != 0) append("${days}d ")
+ if (hours != 0) append("${hours}h ")
+ append("${minutes}m")
+ }
+ }
+}
+
+data class TodayUiState(
+ val localLocation: Location? = null,
+ val locations: List = emptyList(),
+ val selectedLocation: Location? = null,
+
+ val offsetInMinutes: Int = 0,
+ val formattedOffSetInMinutes: String = "",
+ val isFirstItemVisible: Boolean = false,
+ val dateFormatPreference: DateFormatPreference = DateFormatPreference.Default,
+ val locationFormatPreference: LocationFormatPreference = LocationFormatPreference.Default,
+
+ val openAddLocation: Boolean = false,
+ val openEditLocation: Boolean = false,
+ val openEditLocationDialog: Boolean = false,
+
+ val scrollToTop: Boolean = false
+)
+
+sealed interface TodayAction {
+ data object OpenCreateLocation : TodayAction
+ data object CloseEditLocationDialog : TodayAction
+
+ data object OnItemEditClicked : TodayAction
+ data object OnItemDeleteClicked : TodayAction
+ data class OnSelectedLocation(val location: Location) : TodayAction
+ data class OnScrollToTopClicked(val scroll: Boolean) : TodayAction
+ data class OnTimeWheelScrolled(val offsetInMinutes: Int, val isFirstItemVisible: Boolean) : TodayAction
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/form/name/SetupNameScreen.kt b/composeApp/src/commonMain/kotlin/ui/form/name/SetupNameScreen.kt
new file mode 100644
index 0000000..b32a5af
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/form/name/SetupNameScreen.kt
@@ -0,0 +1,112 @@
+package ui.form.name
+
+import androidx.compose.foundation.layout.Column
+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.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Create
+import androidx.compose.material.icons.rounded.TravelExplore
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MediumTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import ui.component.navigationIcon.NavigationBackIcon
+import ui.keyboardShownState
+import ui.theme.DefaultColors
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SetupNameScreen(
+ state: SetupNameUiState,
+ onAction: (SetupNameAction) -> Unit
+) {
+ val scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
+ val localFocusManager = LocalFocusManager.current
+ if (!keyboardShownState().value) localFocusManager.clearFocus()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ MediumTopAppBar(
+ colors = DefaultColors.appBarColors(),
+ title = {
+ Text(
+ if (state.isEditMode) "Edit name" else "Pick a name",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = { NavigationBackIcon { onAction(SetupNameAction.OnBackClicked) } },
+ )
+ },
+ bottomBar = {
+ Column(Modifier.padding(16.dp)) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ enabled = state.continueButtonEnabled,
+ onClick = { onAction(SetupNameAction.OnContinueClicked) }
+ ) {
+ Text("Continue", modifier = Modifier.padding(vertical = 8.dp))
+ }
+ Spacer(Modifier.height(16.dp))
+ }
+ }
+ ) { innerPadding ->
+ Column(Modifier.fillMaxSize().padding(innerPadding).padding(16.dp)) {
+ Text("Enter a name and customise your region name if you'd like.", style = MaterialTheme.typography.bodyLarge)
+ Spacer(modifier = Modifier.weight(1F))
+
+ Text("Name", style = MaterialTheme.typography.labelLarge)
+ Spacer(Modifier.height(8.dp))
+
+ TextField(
+ modifier = Modifier.fillMaxWidth(),
+ colors = DefaultColors.textFieldDefaultColors(),
+ shape = CircleShape,
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Next),
+ value = state.locationName,
+ onValueChange = { onAction(SetupNameAction.OnLocationNameUpdated(it)) },
+ placeholder = { Text("Name") },
+ leadingIcon = {
+ Icon(Icons.Rounded.Create, contentDescription = null)
+ },
+ )
+ Spacer(Modifier.height(16.dp))
+
+ Text("Region Name", style = MaterialTheme.typography.labelLarge)
+ Spacer(Modifier.height(8.dp))
+
+ TextField(
+ modifier = Modifier.fillMaxWidth(),
+ colors = DefaultColors.textFieldDefaultColors(),
+ shape = CircleShape,
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Done),
+ value = state.regionName,
+ onValueChange = { onAction(SetupNameAction.OnRegionNameUpdated(it)) },
+ placeholder = { Text("Region Name") },
+ leadingIcon = { Icon(Icons.Rounded.TravelExplore, contentDescription = null) },
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/form/name/SetupNameViewModel.kt b/composeApp/src/commonMain/kotlin/ui/form/name/SetupNameViewModel.kt
new file mode 100644
index 0000000..ccbfc14
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/form/name/SetupNameViewModel.kt
@@ -0,0 +1,84 @@
+package ui.form.name
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
+import data.locationform.LocationFormRepository
+import data.kairos.KairosRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import nav.Routes
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class SetupNameViewModel : ViewModel(), KoinComponent {
+ private val store: LocationFormRepository by inject()
+ private val kairosRepository: KairosRepository by inject()
+
+ private var _state = MutableStateFlow(SetupNameUiState())
+ val state: StateFlow
+ get() = _state.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ store.observeLocation().first().let { location ->
+ _state.update {
+ it.copy(
+ locationName = location.label ?: "",
+ regionName = (location.regionName ?: "").ifBlank { getFallbackRegionName() ?: "" },
+ isEditMode = location.isEditMode,
+ continueButtonEnabled = state.value.locationName.isNotBlank()
+ )
+ }
+ }
+ }
+ }
+
+ private suspend fun getFallbackRegionName(): String? {
+ return kairosRepository.search(requireNotNull(store.getZoneId().first()))?.city
+ }
+
+ fun onAction(navHostController: NavHostController, action: SetupNameAction) {
+ when (action) {
+ is SetupNameAction.OnLocationNameUpdated -> {
+ _state.update {
+ it.copy(
+ locationName = action.name,
+ continueButtonEnabled = action.name.isNotBlank()
+ )
+ }
+ }
+ is SetupNameAction.OnRegionNameUpdated -> {
+ _state.update { it.copy(regionName = action.name) }
+ }
+ SetupNameAction.OnContinueClicked -> {
+ viewModelScope.launch {
+ store.saveName(state.value.locationName.trimEnd())
+ store.saveRegionName(state.value.regionName.ifBlank { getFallbackRegionName() ?: "" })
+ onAction(navHostController, SetupNameAction.OpenSummary)
+ }
+ }
+ SetupNameAction.OpenSummary -> navHostController.navigate(Routes.FormSummary)
+ SetupNameAction.OnBackClicked -> navHostController.navigateUp()
+ }
+ }
+}
+
+data class SetupNameUiState(
+ var locationName: String = "",
+ val regionName: String = "",
+ val isEditMode: Boolean = false,
+ val continueButtonEnabled: Boolean = false
+)
+
+sealed interface SetupNameAction {
+ data class OnLocationNameUpdated(val name: String) : SetupNameAction
+ data class OnRegionNameUpdated(val name: String) : SetupNameAction
+ data object OnContinueClicked : SetupNameAction
+ data object OpenSummary : SetupNameAction
+ data object OnBackClicked : SetupNameAction
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/form/summary/SummaryScreen.kt b/composeApp/src/commonMain/kotlin/ui/form/summary/SummaryScreen.kt
new file mode 100644
index 0000000..9991b58
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/form/summary/SummaryScreen.kt
@@ -0,0 +1,86 @@
+package ui.form.summary
+
+import androidx.compose.foundation.layout.Column
+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.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MediumTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import data.utils.localTime
+import ui.component.ListItemRow
+import ui.component.navigationIcon.NavigationBackIcon
+import ui.keyboardShownState
+import ui.theme.DefaultColors
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SummaryScreen(
+ state: SummaryUiState,
+ onAction: (SummaryAction) -> Unit
+) {
+ val scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
+ val localFocusManager = LocalFocusManager.current
+ if (!keyboardShownState().value) localFocusManager.clearFocus()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ MediumTopAppBar(
+ colors = DefaultColors.appBarColors(),
+ title = {
+ Text(
+ if (state.isEditMode) "Edit Summary" else "Summary",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ navigationIcon = { NavigationBackIcon { onAction(SummaryAction.OnBackClicked) } },
+ )
+ },
+ bottomBar = {
+ Column(Modifier.padding(16.dp)) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { onAction(SummaryAction.OnSubmitClicked) }
+ ) {
+ Text(
+ if (state.isEditMode) "Save Changes" else "Create",
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ }
+ Spacer(Modifier.height(16.dp))
+ }
+ }
+ ) { innerPadding ->
+ Column(Modifier.fillMaxSize().padding(innerPadding).padding(16.dp)) {
+ Text(
+ "Here is what it will look like in your dashboard. You can still edit it later.",
+ style = MaterialTheme.typography.bodyLarge
+ )
+
+ Spacer(modifier = Modifier.weight(1F))
+
+ state.location?.let {
+ ListItemRow(
+ label = it.name,
+ description = it.regionName,
+ endLabel = it.zone.localTime()
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/form/summary/SummaryViewModel.kt b/composeApp/src/commonMain/kotlin/ui/form/summary/SummaryViewModel.kt
new file mode 100644
index 0000000..6d252e5
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/form/summary/SummaryViewModel.kt
@@ -0,0 +1,78 @@
+package ui.form.summary
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
+import data.location.LocationRepository
+import data.locationform.LocationFormRepository
+import data.location.model.Location
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import nav.Routes
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import randomUUID
+
+class SummaryViewModel : ViewModel(), KoinComponent {
+ private val store: LocationFormRepository by inject()
+ private val locationRepository: LocationRepository by inject()
+
+ private var _state = MutableStateFlow(SummaryUiState())
+ val state: StateFlow
+ get() = _state.asStateFlow()
+
+ init {
+ viewModelScope.launch {
+ store.observeLocation().first().let { newLocationData ->
+ _state.update {
+ it.copy(
+ isEditMode = newLocationData.isEditMode,
+ location = Location(
+ id = newLocationData.id ?: randomUUID(),
+ name = newLocationData.label ?: "",
+ regionName = newLocationData.regionName ?: "",
+ zoneId = newLocationData.zoneId ?: ""
+ )
+ )
+ }
+ }
+ }
+ }
+
+ fun onAction(navHostController: NavHostController, action: SummaryAction) {
+ when (action) {
+ SummaryAction.OnSubmitClicked -> {
+ viewModelScope.launch {
+ val location = state.value.location
+ if (location != null) {
+ if (state.value.isEditMode) {
+ locationRepository.updateLocation(location)
+ } else {
+ locationRepository.addLocation(location)
+ }
+ }
+ store.clear()
+ onAction(navHostController, SummaryAction.OpenDashboard)
+ }
+ }
+ SummaryAction.OpenDashboard -> navHostController.popBackStack(Routes.Dashboard, inclusive = false)
+ SummaryAction.OnBackClicked -> navHostController.navigateUp()
+ }
+ }
+}
+
+data class SummaryUiState(
+ val title: String = "",
+ val location: Location? = null,
+ val isEditMode: Boolean = false,
+)
+
+sealed interface SummaryAction {
+ data object OnSubmitClicked : SummaryAction
+ data object OpenDashboard : SummaryAction
+ data object OnBackClicked : SummaryAction
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/form/zone/SelectZoneScreen.kt b/composeApp/src/commonMain/kotlin/ui/form/zone/SelectZoneScreen.kt
new file mode 100644
index 0000000..5886fb0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/form/zone/SelectZoneScreen.kt
@@ -0,0 +1,205 @@
+package ui.form.zone
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Clear
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material3.Button
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MediumTopAppBar
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.TopAppBarScrollBehavior
+import androidx.compose.material3.rememberTopAppBarState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import ui.component.IconButton
+import ui.component.ListItemRow
+import ui.component.navigationIcon.NavigationBackIcon
+import ui.theme.DefaultColors
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SelectZoneScreen(
+ state: SelectZoneUiState,
+ onAction: (SelectZoneAction) -> Unit
+) {
+ val scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ topBar = {
+ if (state.searchMode) {
+ SearchAppBar(state, onAction)
+ } else {
+ AppBar(scrollBehavior, state, onAction)
+ }
+ },
+ ) { innerPadding ->
+ val searching = state.searching
+ if (searching) {
+ Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
+ CircularProgressIndicator(Modifier.align(Alignment.Center))
+ }
+ } else {
+ LoadedSelectZoneScreen(innerPadding, state, onAction)
+ }
+ }
+}
+
+@Composable
+private fun LoadedSelectZoneScreen(
+ innerPadding: PaddingValues,
+ state: SelectZoneUiState,
+ onAction: (SelectZoneAction) -> Unit
+) {
+ val list = state.zones
+ val lazyListState = rememberLazyListState()
+ Box(Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().padding(top = innerPadding.calculateTopPadding()),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ state = lazyListState
+ ) {
+ if (!state.searchMode) {
+ item {
+ Text(
+ "Choose your time zone from the list below. Use the search bar to quickly find your specific time zone.",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ Spacer(Modifier.height(16.dp))
+ }
+ }
+ items(count = list.size, key = { list[it].id }) { index ->
+ val item = list[index]
+ val selected = state.selectedZone?.zoneIdString == item.zoneIdString
+ ListItemRow(
+ label = item.city,
+ description = item.zone,
+ endLabel = item.localTime(),
+ selected = selected,
+ onClicked = { onAction(SelectZoneAction.OnZoneSelected(item)) }
+ )
+ }
+ }
+ Column(
+ Modifier
+ .background(brush = DefaultColors.bottomScaffoldBrush())
+ .padding(innerPadding)
+ .padding(horizontal = 16.dp)
+ .align(Alignment.BottomStart)
+ ) {
+ Button(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { onAction(SelectZoneAction.OnContinueClicked) },
+ enabled = state.continueButtonEnabled
+ ) {
+ Text("Continue", modifier = Modifier.padding(vertical = 8.dp))
+ }
+ Spacer(Modifier.height(16.dp))
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun AppBar(
+ scrollBehavior: TopAppBarScrollBehavior,
+ state: SelectZoneUiState,
+ onAction: (SelectZoneAction) -> Unit
+) {
+ MediumTopAppBar(
+ colors = DefaultColors.appBarColors(),
+ title = {
+ Text(
+ if (state.isEditMode) "Edit your time zone" else "Select your time zone",
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1
+ )
+ },
+ navigationIcon = { NavigationBackIcon { onAction(SelectZoneAction.OnBackClicked) } },
+ actions = {
+ IconButton(
+ imageVector = Icons.Rounded.Search,
+ contentDescription = "Search",
+ onClicked = { onAction(SelectZoneAction.OnSearchModeUpdated(searchMode = true)) }
+ )
+ },
+ scrollBehavior = scrollBehavior
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SearchAppBar(
+ state: SelectZoneUiState,
+ onAction: (SelectZoneAction) -> Unit
+) {
+ val focusRequester = remember { FocusRequester() }
+ TopAppBar(
+ colors = DefaultColors.appBarColors(),
+ title = {
+ TextField(
+ modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Done),
+ value = state.query,
+ onValueChange = { onAction(SelectZoneAction.OnQueryUpdated(it)) },
+ placeholder = { Text(text = "Search") },
+ trailingIcon = {
+ if (state.query.isNotEmpty()) {
+ Icon(
+ modifier = Modifier.clickable { onAction(SelectZoneAction.OnQueryUpdated("")) },
+ imageVector = Icons.Rounded.Clear,
+ contentDescription = null
+ )
+ }
+ },
+ )
+ },
+ navigationIcon = {
+ if (state.query.isNotBlank()) {
+ NavigationBackIcon { onAction(SelectZoneAction.OnBackClicked) }
+ } else {
+ IconButton(
+ imageVector = Icons.Rounded.Close,
+ contentDescription = "Close",
+ onClicked = { onAction(SelectZoneAction.OnSearchModeUpdated(searchMode = false)) }
+ )
+ }
+ },
+ )
+ LaunchedEffect(state.searchMode) {
+ if (state.searchMode) focusRequester.requestFocus()
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/form/zone/SelectZoneViewModel.kt b/composeApp/src/commonMain/kotlin/ui/form/zone/SelectZoneViewModel.kt
new file mode 100644
index 0000000..36a2aa1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/form/zone/SelectZoneViewModel.kt
@@ -0,0 +1,126 @@
+package ui.form.zone
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.NavHostController
+import data.kairos.KairosRepository
+import data.kairos.model.Zone
+import data.locationform.LocationFormRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import nav.Routes
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class SelectZoneViewModel : ViewModel(), KoinComponent {
+ companion object {
+ private val popularZones = listOf(
+ "Asia/Kuala_Lumpur",
+ "Asia/Tokyo",
+ "Asia/Kolkata",
+ "Europe/Amsterdam",
+ "Europe/London",
+ "Europe/Kyiv",
+ "Australia/Melbourne",
+ "America/New_York",
+ "America/Los_Angeles",
+ "Pacific/Auckland",
+ )
+ }
+
+ private val store: LocationFormRepository by inject()
+ private val kairosRepository: KairosRepository by inject()
+
+ private var _state = MutableStateFlow(SelectZoneUiState())
+ val state: StateFlow
+ get() = _state.asStateFlow()
+
+ private val zones get() = kairosRepository.zones.sorted()
+ private fun List.sorted(): List = sortedWith(
+ compareBy(
+ { it.zoneIdString != state.value.selectedZone?.zoneIdString },
+ { it.zoneIdString !in popularZones },
+ )
+ )
+
+ init {
+ viewModelScope.launch {
+ store.observeLocation().first().let { newLocationData ->
+ val selectedZone = kairosRepository.search(newLocationData.zoneId)
+ _state.update {
+ it.copy(
+ isEditMode = newLocationData.isEditMode,
+ selectedZone = selectedZone,
+ continueButtonEnabled = selectedZone != null,
+ )
+ }
+ _state.update {
+ it.copy(zones = kairosRepository.zones.sorted())
+ }
+ }
+ }
+ }
+
+ fun onAction(navHostController: NavHostController, action: SelectZoneAction) {
+ when (action) {
+ is SelectZoneAction.OnSearchModeUpdated -> {
+ _state.update { it.copy(searchMode = action.searchMode) }
+ }
+ is SelectZoneAction.OnQueryUpdated -> {
+ _state.update {
+ it.copy(
+ query = action.query,
+ zones = if (action.query.isBlank()) {
+ zones.sorted()
+ } else {
+ val trimmedQuery = action.query.replace("\\s+".toRegex(), "")
+ zones.filter { region -> region.doesMatchSearchQuery(trimmedQuery) }
+ }
+ )
+ }
+ }
+ is SelectZoneAction.OnZoneSelected -> {
+ _state.update {
+ it.copy(
+ selectedZone = action.zone,
+ continueButtonEnabled = true
+ )
+ }
+ }
+ SelectZoneAction.OnContinueClicked -> {
+ viewModelScope.launch {
+ val selectedZone = state.value.selectedZone
+ if (selectedZone != null) {
+ store.saveZoneId(selectedZone.zoneIdString)
+ onAction(navHostController, SelectZoneAction.OpenSetupName)
+ }
+ }
+ }
+ SelectZoneAction.OpenSetupName -> navHostController.navigate(Routes.FormSetupName)
+ SelectZoneAction.OnBackClicked -> navHostController.navigateUp()
+ }
+ }
+}
+
+data class SelectZoneUiState(
+ val query: String = "",
+ val searching: Boolean = false,
+ val searchMode: Boolean = false,
+ val zones: List = listOf(),
+ val isEditMode: Boolean = false,
+ val selectedZone: Zone? = null,
+ val continueButtonEnabled: Boolean = false
+)
+
+sealed interface SelectZoneAction {
+ data class OnSearchModeUpdated(val searchMode: Boolean) : SelectZoneAction
+ data class OnQueryUpdated(val query: String) : SelectZoneAction
+ data class OnZoneSelected(val zone: Zone) : SelectZoneAction
+ data object OnContinueClicked : SelectZoneAction
+ data object OpenSetupName : SelectZoneAction
+ data object OnBackClicked : SelectZoneAction
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/theme/Color.kt b/composeApp/src/commonMain/kotlin/ui/theme/Color.kt
new file mode 100644
index 0000000..aa9eddb
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/theme/Color.kt
@@ -0,0 +1,75 @@
+package ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val primaryLight = Color(0xFF8B5000)
+val onPrimaryLight = Color(0xFFFFFFFF)
+val primaryContainerLight = Color(0xFFFFA747)
+val onPrimaryContainerLight = Color(0xFF462600)
+val secondaryLight = Color(0xFF815526)
+val onSecondaryLight = Color(0xFFFFFFFF)
+val secondaryContainerLight = Color(0xFFFFCB9B)
+val onSecondaryContainerLight = Color(0xFF5D3709)
+val tertiaryLight = Color(0xFF3E1912)
+val onTertiaryLight = Color(0xFFFFFFFF)
+val tertiaryContainerLight = Color(0xFF653930)
+val onTertiaryContainerLight = Color(0xFFFFD4CB)
+val errorLight = Color(0xFFB02F00)
+val onErrorLight = Color(0xFFFFFFFF)
+val errorContainerLight = Color(0xFFFF7147)
+val onErrorContainerLight = Color(0xFF240400)
+val backgroundLight = Color(0xFFFFF8F5)
+val onBackgroundLight = Color(0xFF231A12)
+val surfaceLight = Color(0xFFFFF8F5)
+val onSurfaceLight = Color(0xFF231A12)
+val surfaceVariantLight = Color(0xFFF8DEC9)
+val onSurfaceVariantLight = Color(0xFF554434)
+val outlineLight = Color(0xFF887362)
+val outlineVariantLight = Color(0xFFDBC2AE)
+val scrimLight = Color(0xFF000000)
+val inverseSurfaceLight = Color(0xFF392E25)
+val inverseOnSurfaceLight = Color(0xFFFFEEE1)
+val inversePrimaryLight = Color(0xFFFFB871)
+val surfaceDimLight = Color(0xFFE9D7C9)
+val surfaceBrightLight = Color(0xFFFFF8F5)
+val surfaceContainerLowestLight = Color(0xFFFFFFFF)
+val surfaceContainerLowLight = Color(0xFFFFF1E7)
+val surfaceContainerLight = Color(0xFFFDEBDD)
+val surfaceContainerHighLight = Color(0xFFF7E5D7)
+val surfaceContainerHighestLight = Color(0xFFF1DFD2)
+
+val primaryDark = Color(0xFFFFCB9A)
+val onPrimaryDark = Color(0xFF4A2800)
+val primaryContainerDark = Color(0xFFF89305)
+val onPrimaryContainerDark = Color(0xFF341A00)
+val secondaryDark = Color(0xFFF6BB83)
+val onSecondaryDark = Color(0xFF4A2800)
+val secondaryContainerDark = Color(0xFF5D3709)
+val onSecondaryContainerDark = Color(0xFFFFCB9A)
+val tertiaryDark = Color(0xFFF7B7AA)
+val onTertiaryDark = Color(0xFF4D251D)
+val tertiaryContainerDark = Color(0xFF49221A)
+val onTertiaryContainerDark = Color(0xFFEAAC9E)
+val errorDark = Color(0xFFFFB5A0)
+val onErrorDark = Color(0xFF5F1500)
+val errorContainerDark = Color(0xFFD73B00)
+val onErrorContainerDark = Color(0xFFFFFFFF)
+val backgroundDark = Color(0xFF1A120A)
+val onBackgroundDark = Color(0xFFF1DFD2)
+val surfaceDark = Color(0xFF1A120A)
+val onSurfaceDark = Color(0xFFF1DFD2)
+val surfaceVariantDark = Color(0xFF554434)
+val onSurfaceVariantDark = Color(0xFFDBC2AE)
+val outlineDark = Color(0xFFA38D7A)
+val outlineVariantDark = Color(0xFF554434)
+val scrimDark = Color(0xFF000000)
+val inverseSurfaceDark = Color(0xFFF1DFD2)
+val inverseOnSurfaceDark = Color(0xFF392E25)
+val inversePrimaryDark = Color(0xFF8B5000)
+val surfaceDimDark = Color(0xFF1A120A)
+val surfaceBrightDark = Color(0xFF42372E)
+val surfaceContainerLowestDark = Color(0xFF140D06)
+val surfaceContainerLowDark = Color(0xFF231A12)
+val surfaceContainerDark = Color(0xFF271E15)
+val surfaceContainerHighDark = Color(0xFF32281F)
+val surfaceContainerHighestDark = Color(0xFF3D3329)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/theme/DefaultColors.kt b/composeApp/src/commonMain/kotlin/ui/theme/DefaultColors.kt
new file mode 100644
index 0000000..e986947
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/theme/DefaultColors.kt
@@ -0,0 +1,35 @@
+package ui.theme
+
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+
+object DefaultColors {
+ @Composable
+ fun textFieldDefaultColors() = TextFieldDefaults.colors(
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent,
+ errorIndicatorColor = Color.Transparent
+ )
+
+ @OptIn(ExperimentalMaterial3Api::class)
+ @Composable
+ fun appBarColors() = TopAppBarDefaults.topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ scrolledContainerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ )
+
+ @Composable
+ fun bottomScaffoldBrush() = Brush.verticalGradient(
+ colors = listOf(
+ Color.Transparent,
+ MaterialTheme.colorScheme.surfaceDim,
+ ),
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/theme/Theme.kt b/composeApp/src/commonMain/kotlin/ui/theme/Theme.kt
new file mode 100644
index 0000000..6d4e3d3
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/theme/Theme.kt
@@ -0,0 +1,103 @@
+package ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import data.preferences.model.ThemePreference
+
+private val lightScheme = lightColorScheme(
+ primary = primaryLight,
+ onPrimary = onPrimaryLight,
+ primaryContainer = primaryContainerLight,
+ onPrimaryContainer = onPrimaryContainerLight,
+ secondary = secondaryLight,
+ onSecondary = onSecondaryLight,
+ secondaryContainer = secondaryContainerLight,
+ onSecondaryContainer = onSecondaryContainerLight,
+ tertiary = tertiaryLight,
+ onTertiary = onTertiaryLight,
+ tertiaryContainer = tertiaryContainerLight,
+ onTertiaryContainer = onTertiaryContainerLight,
+ error = errorLight,
+ onError = onErrorLight,
+ errorContainer = errorContainerLight,
+ onErrorContainer = onErrorContainerLight,
+ background = backgroundLight,
+ onBackground = onBackgroundLight,
+ surface = surfaceLight,
+ onSurface = onSurfaceLight,
+ surfaceVariant = surfaceVariantLight,
+ onSurfaceVariant = onSurfaceVariantLight,
+ outline = outlineLight,
+ outlineVariant = outlineVariantLight,
+ scrim = scrimLight,
+ inverseSurface = inverseSurfaceLight,
+ inverseOnSurface = inverseOnSurfaceLight,
+ inversePrimary = inversePrimaryLight,
+ surfaceDim = surfaceDimLight,
+ surfaceBright = surfaceBrightLight,
+ surfaceContainerLowest = surfaceContainerLowestLight,
+ surfaceContainerLow = surfaceContainerLowLight,
+ surfaceContainer = surfaceContainerLight,
+ surfaceContainerHigh = surfaceContainerHighLight,
+ surfaceContainerHighest = surfaceContainerHighestLight,
+)
+
+private val darkScheme = darkColorScheme(
+ primary = primaryDark,
+ onPrimary = onPrimaryDark,
+ primaryContainer = primaryContainerDark,
+ onPrimaryContainer = onPrimaryContainerDark,
+ secondary = secondaryDark,
+ onSecondary = onSecondaryDark,
+ secondaryContainer = secondaryContainerDark,
+ onSecondaryContainer = onSecondaryContainerDark,
+ tertiary = tertiaryDark,
+ onTertiary = onTertiaryDark,
+ tertiaryContainer = tertiaryContainerDark,
+ onTertiaryContainer = onTertiaryContainerDark,
+ error = errorDark,
+ onError = onErrorDark,
+ errorContainer = errorContainerDark,
+ onErrorContainer = onErrorContainerDark,
+ background = backgroundDark,
+ onBackground = onBackgroundDark,
+ surface = surfaceDark,
+ onSurface = onSurfaceDark,
+ surfaceVariant = surfaceVariantDark,
+ onSurfaceVariant = onSurfaceVariantDark,
+ outline = outlineDark,
+ outlineVariant = outlineVariantDark,
+ scrim = scrimDark,
+ inverseSurface = inverseSurfaceDark,
+ inverseOnSurface = inverseOnSurfaceDark,
+ inversePrimary = inversePrimaryDark,
+ surfaceDim = surfaceDimDark,
+ surfaceBright = surfaceBrightDark,
+ surfaceContainerLowest = surfaceContainerLowestDark,
+ surfaceContainerLow = surfaceContainerLowDark,
+ surfaceContainer = surfaceContainerDark,
+ surfaceContainerHigh = surfaceContainerHighDark,
+ surfaceContainerHighest = surfaceContainerHighestDark,
+)
+
+@Composable
+fun AppTheme(
+ theme: ThemePreference = ThemePreference.Default,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when (theme) {
+ ThemePreference.System -> if (isSystemInDarkTheme()) darkScheme else lightScheme
+ ThemePreference.Light -> lightScheme
+ ThemePreference.Dark -> darkScheme
+ }
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = AppTypography,
+ shapes = AppShapes,
+ content = content
+ )
+}
+
diff --git a/composeApp/src/commonMain/kotlin/ui/theme/TwilightModifiers.kt b/composeApp/src/commonMain/kotlin/ui/theme/TwilightModifiers.kt
new file mode 100644
index 0000000..a0ef356
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/theme/TwilightModifiers.kt
@@ -0,0 +1,12 @@
+package ui.theme
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+object TwilightModifiers {
+ val iconModifier = Modifier.padding(
+ horizontal = 16.dp,
+ vertical = 20.dp,
+ )
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/ui/theme/Type.kt b/composeApp/src/commonMain/kotlin/ui/theme/Type.kt
new file mode 100644
index 0000000..4475a3c
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/ui/theme/Type.kt
@@ -0,0 +1,59 @@
+package ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Shapes
+import androidx.compose.material3.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.resources.Font
+import twilight.composeapp.generated.resources.Res
+import twilight.composeapp.generated.resources.lato_bold
+import twilight.composeapp.generated.resources.lato_regular
+import twilight.composeapp.generated.resources.league_spartan_regular
+
+val displayFontFamily: FontFamily
+ @Composable
+ get() = FontFamily(
+ Font(Res.font.league_spartan_regular, weight = FontWeight.Normal)
+ )
+
+val bodyFontFamily: FontFamily
+ @Composable get() = FontFamily(
+ Font(Res.font.lato_bold, weight = FontWeight.Bold),
+ Font(Res.font.lato_regular, weight = FontWeight.Normal),
+ )
+
+val AppTypography: Typography
+ @Composable get() {
+ val baseline = Typography()
+ val displayFontFamily = displayFontFamily
+ val bodyFontFamily = bodyFontFamily
+
+ return Typography(
+ displayLarge = baseline.displayLarge.copy(fontFamily = displayFontFamily),
+ displayMedium = baseline.displayMedium.copy(fontFamily = displayFontFamily),
+ displaySmall = baseline.displaySmall.copy(fontFamily = displayFontFamily),
+ headlineLarge = baseline.headlineLarge.copy(fontFamily = displayFontFamily),
+ headlineMedium = baseline.headlineMedium.copy(fontFamily = displayFontFamily),
+ headlineSmall = baseline.headlineSmall.copy(fontFamily = displayFontFamily),
+ titleLarge = baseline.titleLarge.copy(fontFamily = displayFontFamily),
+ titleMedium = baseline.titleMedium.copy(fontFamily = displayFontFamily),
+ titleSmall = baseline.titleSmall.copy(fontFamily = displayFontFamily),
+ bodyLarge = baseline.bodyLarge.copy(fontFamily = bodyFontFamily),
+ bodyMedium = baseline.bodyMedium.copy(fontFamily = bodyFontFamily),
+ bodySmall = baseline.bodySmall.copy(fontFamily = bodyFontFamily),
+ labelLarge = baseline.labelLarge.copy(fontFamily = bodyFontFamily, fontWeight = FontWeight.Bold),
+ labelMedium = baseline.labelMedium.copy(fontFamily = bodyFontFamily),
+ labelSmall = baseline.labelSmall.copy(fontFamily = bodyFontFamily),
+ )
+ }
+
+val AppShapes = Shapes(
+ extraSmall = RoundedCornerShape(4.dp),
+ small = RoundedCornerShape(8.dp),
+ medium = RoundedCornerShape(12.dp),
+ large = RoundedCornerShape(16.dp),
+ extraLarge = RoundedCornerShape(24.dp)
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/sqldelight/com/delacrixmorgan/twilight/database/LocationEntity.sq b/composeApp/src/commonMain/sqldelight/com/delacrixmorgan/twilight/database/LocationEntity.sq
new file mode 100644
index 0000000..69d0cac
--- /dev/null
+++ b/composeApp/src/commonMain/sqldelight/com/delacrixmorgan/twilight/database/LocationEntity.sq
@@ -0,0 +1,28 @@
+CREATE TABLE LocationEntity (
+ id TEXT NOT NULL ,
+ label TEXT NOT NULL,
+ regionName TEXT NOT NULL,
+ zoneIdString TEXT NOT NULL,
+ PRIMARY KEY(id)
+);
+
+insertItem:
+INSERT OR REPLACE INTO LocationEntity(id, label, regionName, zoneIdString)
+VALUES(?,?,?,?);
+
+selectAll:
+SELECT * FROM LocationEntity;
+
+selectById:
+SELECT * FROM LocationEntity WHERE id = ?;
+
+update:
+UPDATE LocationEntity
+SET label = ?, regionName = ?, zoneIdString = ?
+WHERE id = ?;
+
+deleteById:
+DELETE FROM LocationEntity WHERE id = ?;
+
+deleteAll:
+DELETE FROM LocationEntity;
\ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/KairosRepositoryTest.kt b/composeApp/src/commonTest/kotlin/KairosRepositoryTest.kt
new file mode 100644
index 0000000..0ef7152
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/KairosRepositoryTest.kt
@@ -0,0 +1,117 @@
+import data.kairos.KairosRepository
+import data.utils.convert
+import kotlinx.datetime.LocalDateTime
+import org.koin.core.component.inject
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.test.KoinTest
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class KairosRepositoryTest : KoinTest {
+
+ private val kairosRepository: KairosRepository by inject()
+
+ private val netherlandsZone by lazy { kairosRepository.search("Europe/Amsterdam")!! }
+ private val malaysiaZone by lazy { kairosRepository.search("Asia/Kuala_Lumpur")!! }
+
+ @BeforeTest
+ fun setup() {
+ startKoin { modules(repositoryModules) }
+ }
+
+ @AfterTest
+ fun tearDown() {
+ stopKoin()
+ }
+
+ /**
+ * Initialisation
+ */
+ @Test
+ fun `Given time with DST When set to Malaysia Time Then return time`() {
+ val expected = "2024-01-01T00:00"
+ val localDateTime: LocalDateTime = LocalDateTime.parse(expected)
+ val actualLocalDateTime: LocalDateTime = localDateTime.convert(
+ from = malaysiaZone,
+ to = malaysiaZone
+ )
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ /**
+ * Netherlands to Malaysia Time
+ */
+ @Test
+ fun `Given Netherlands Time with DST When convert to Malaysia Time Then return Malaysia Time`() {
+ val netherlandsDST = "2024-01-01T00:00"
+ val expected = "2024-01-01T07:00"
+ val netherlandsDSTLocalDateTime: LocalDateTime = LocalDateTime.parse(netherlandsDST)
+ val actualLocalDateTime: LocalDateTime = netherlandsDSTLocalDateTime.convert(
+ from = netherlandsZone,
+ to = malaysiaZone
+ )
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ @Test
+ fun `Given Netherlands Time without DST When convert to Malaysia Time Then return Malaysia Time`() {
+ val netherlandsDST = "2024-06-01T00:00"
+ val expected = "2024-06-01T06:00"
+ val netherlandsDSTLocalDateTime: LocalDateTime = LocalDateTime.parse(netherlandsDST)
+ val actualLocalDateTime: LocalDateTime = netherlandsDSTLocalDateTime.convert(
+ from = netherlandsZone,
+ to = malaysiaZone,
+ )
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ /**
+ * Malaysia to Netherlands Time
+ */
+ @Test
+ fun `Given Malaysia Time When convert to Netherlands with DST Then return Netherlands DST`() {
+ val malaysiaTime = "2024-01-01T07:00"
+ val expected = "2024-01-01T00:00"
+ val malaysiaLocalDateTime: LocalDateTime = LocalDateTime.parse(malaysiaTime)
+ val actualLocalDateTime: LocalDateTime = malaysiaLocalDateTime.convert(
+ from = malaysiaZone,
+ to = netherlandsZone
+ )
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ @Test
+ fun `Given Malaysia Time When convert to Netherlands without DST Then return Netherlands without DST`() {
+ val malaysiaTime = "2024-06-01T06:00"
+ val expected = "2024-06-01T00:00"
+ val malaysiaLocalDateTime: LocalDateTime = LocalDateTime.parse(malaysiaTime)
+ val actualLocalDateTime: LocalDateTime = malaysiaLocalDateTime.convert(
+ from = malaysiaZone,
+ to = netherlandsZone
+ )
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/KoinTestModules.kt b/composeApp/src/commonTest/kotlin/KoinTestModules.kt
new file mode 100644
index 0000000..d10281a
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/KoinTestModules.kt
@@ -0,0 +1,4 @@
+import data.kairos.KairosRepository
+import org.koin.dsl.module
+
+val repositoryModules = module { single { KairosRepository() } }
\ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/LocalDateTimeTest.kt b/composeApp/src/commonTest/kotlin/LocalDateTimeTest.kt
new file mode 100644
index 0000000..100e41d
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/LocalDateTimeTest.kt
@@ -0,0 +1,94 @@
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class LocalDateTimeTest {
+ private val netherlandsTimeZone = TimeZone.of("Europe/Amsterdam")
+ private val malaysiaTimeZone = TimeZone.of("Asia/Kuala_Lumpur")
+
+ /**
+ * Initialisation
+ */
+ @Test
+ fun `Given time with DST When set to Malaysia Time Then return time`() {
+ val expected = "2024-01-01T00:00"
+ val localDateTime: LocalDateTime = LocalDateTime.parse(expected)
+ val actualLocalDateTime: LocalDateTime = localDateTime
+ .toInstant(malaysiaTimeZone)
+ .toLocalDateTime(malaysiaTimeZone)
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ /**
+ * Netherlands to Malaysia Time
+ */
+ @Test
+ fun `Given Netherlands Time with DST When convert to Malaysia Time Then return Malaysia Time`() {
+ val netherlandsDST = "2024-01-01T00:00"
+ val expected = "2024-01-01T07:00"
+ val netherlandsDSTLocalDateTime: LocalDateTime = LocalDateTime.parse(netherlandsDST)
+ val actualLocalDateTime: LocalDateTime = netherlandsDSTLocalDateTime
+ .toInstant(netherlandsTimeZone)
+ .toLocalDateTime(malaysiaTimeZone)
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ @Test
+ fun `Given Netherlands Time without DST When convert to Malaysia Time Then return Malaysia Time`() {
+ val netherlandsDST = "2024-06-01T00:00"
+ val expected = "2024-06-01T06:00"
+ val netherlandsDSTLocalDateTime: LocalDateTime = LocalDateTime.parse(netherlandsDST)
+ val actualLocalDateTime: LocalDateTime = netherlandsDSTLocalDateTime
+ .toInstant(netherlandsTimeZone)
+ .toLocalDateTime(malaysiaTimeZone)
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ /**
+ * Malaysia to Netherlands Time
+ */
+ @Test
+ fun `Given Malaysia Time When convert to Netherlands with DST Then return Netherlands DST`() {
+ val malaysiaTime = "2024-01-01T07:00"
+ val expected = "2024-01-01T00:00"
+ val malaysiaLocalDateTime: LocalDateTime = LocalDateTime.parse(malaysiaTime)
+ val actualLocalDateTime: LocalDateTime = malaysiaLocalDateTime
+ .toInstant(malaysiaTimeZone)
+ .toLocalDateTime(netherlandsTimeZone)
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+
+ @Test
+ fun `Given Malaysia Time When convert to Netherlands without DST Then return Netherlands without DST`() {
+ val malaysiaTime = "2024-06-01T06:00"
+ val expected = "2024-06-01T00:00"
+ val malaysiaLocalDateTime: LocalDateTime = LocalDateTime.parse(malaysiaTime)
+ val actualLocalDateTime: LocalDateTime = malaysiaLocalDateTime
+ .toInstant(malaysiaTimeZone)
+ .toLocalDateTime(netherlandsTimeZone)
+
+ assertEquals(
+ expected = expected,
+ actual = actualLocalDateTime.toString()
+ )
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/mapper/AmericaZoneIdToZoneMapperTest.kt b/composeApp/src/commonTest/kotlin/mapper/AmericaZoneIdToZoneMapperTest.kt
new file mode 100644
index 0000000..7cf5a0b
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/mapper/AmericaZoneIdToZoneMapperTest.kt
@@ -0,0 +1,42 @@
+package mapper
+
+import data.kairos.model.Region
+import data.kairos.KairosRepository
+import org.koin.core.component.inject
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.test.KoinTest
+import repositoryModules
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class AmericaZoneIdToZoneMapperTest : KoinTest {
+
+ private val kairosRepository: KairosRepository by inject()
+
+ @BeforeTest
+ fun setup() {
+ startKoin { modules(repositoryModules) }
+ }
+
+ @AfterTest
+ fun tearDown() {
+ stopKoin()
+ }
+
+ private val zoneIds by lazy {
+ kairosRepository.zones
+ .filter { it.region == Region.America }
+ .map { it.timeZone.toString() }
+ }
+
+ @Test
+ fun `Given America zoneIdStrings When mapping Then it should contain them`() {
+ val actualZoneIdStrings = listOf(
+ "America/New_York",
+ )
+ assertTrue(zoneIds.containsAll(actualZoneIdStrings), "Missing: ${actualZoneIdStrings.minus(zoneIds.toSet())}")
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/mapper/AsiaZoneIdToZoneMapperTest.kt b/composeApp/src/commonTest/kotlin/mapper/AsiaZoneIdToZoneMapperTest.kt
new file mode 100644
index 0000000..adae54e
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/mapper/AsiaZoneIdToZoneMapperTest.kt
@@ -0,0 +1,84 @@
+package mapper
+
+import data.kairos.model.Region
+import data.kairos.KairosRepository
+import org.koin.core.component.inject
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.test.KoinTest
+import repositoryModules
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class AsiaZoneIdToZoneMapperTest : KoinTest {
+
+ private val kairosRepository: KairosRepository by inject()
+
+ private val zoneIds by lazy {
+ kairosRepository.zones
+ .filter { it.region == Region.Asia }
+ .map { it.timeZone.toString() }
+ }
+
+ @BeforeTest
+ fun setup() {
+ startKoin { modules(repositoryModules) }
+ }
+
+ @AfterTest
+ fun tearDown() {
+ stopKoin()
+ }
+
+ @Test
+ fun `Given South East Asia zoneIdStrings When mapping Then it should contain them`() {
+ val actualZoneIdStrings = listOf(
+ "Asia/Brunei",
+ "Asia/Phnom_Penh",
+ "Asia/Jakarta",
+ "Asia/Makassar",
+ "Asia/Jayapura",
+ "Asia/Vientiane",
+ "Asia/Kuala_Lumpur",
+ "Asia/Kuching",
+ "Asia/Yangon",
+ "Asia/Manila",
+ "Asia/Singapore",
+ "Asia/Bangkok",
+ "Asia/Dili",
+ "Asia/Ho_Chi_Minh",
+ "Asia/Saigon",
+ )
+ assertTrue(zoneIds.containsAll(actualZoneIdStrings), "Missing: ${actualZoneIdStrings.minus(zoneIds.toSet())}")
+ }
+
+ @Test
+ fun `Given East Asia zoneIdStrings When mapping Then it should contain them`() {
+ val actualZoneIdStrings = listOf(
+ "Asia/Shanghai",
+ "Asia/Urumqi",
+ "Asia/Tokyo",
+ "Asia/Seoul",
+ "Asia/Pyongyang",
+ "Asia/Ulaanbaatar",
+ "Asia/Taipei"
+ )
+ assertTrue(zoneIds.containsAll(actualZoneIdStrings), "Missing: ${actualZoneIdStrings.minus(zoneIds.toSet())}")
+ }
+
+ @Test
+ fun `Given South Asia zoneIdStrings When mapping Then it should contain them`() {
+ val actualZoneIdStrings = listOf(
+ "Asia/Kabul",
+ "Asia/Dhaka",
+ "Asia/Thimphu",
+ "Asia/Kolkata",
+ "Asia/Kathmandu",
+ "Asia/Karachi",
+ "Asia/Colombo"
+ )
+ assertTrue(zoneIds.containsAll(actualZoneIdStrings), "Missing: ${actualZoneIdStrings.minus(zoneIds.toSet())}")
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/mapper/AustraliaZoneIdToZoneMapperTest.kt b/composeApp/src/commonTest/kotlin/mapper/AustraliaZoneIdToZoneMapperTest.kt
new file mode 100644
index 0000000..45cd79c
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/mapper/AustraliaZoneIdToZoneMapperTest.kt
@@ -0,0 +1,50 @@
+package mapper
+
+import data.kairos.model.Region
+import data.kairos.KairosRepository
+import org.koin.core.component.inject
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.test.KoinTest
+import repositoryModules
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class AustraliaZoneIdToZoneMapperTest : KoinTest {
+
+ private val kairosRepository: KairosRepository by inject()
+
+ private val zoneIds by lazy {
+ kairosRepository.zones
+ .filter { it.region == Region.Australia }
+ .map { it.timeZone.toString() }
+ }
+
+ @BeforeTest
+ fun setup() {
+ startKoin { modules(repositoryModules) }
+ }
+
+ @AfterTest
+ fun tearDown() {
+ stopKoin()
+ }
+
+ @Test
+ fun `Given Australia zoneIdStrings When mapping Then it should contain them`() {
+ val actualZoneIdStrings = listOf(
+ "Australia/Adelaide",
+ "Australia/Brisbane",
+ "Australia/Broken_Hill",
+ "Australia/Darwin",
+ "Australia/Hobart",
+ "Australia/Lord_Howe",
+ "Australia/Melbourne",
+ "Australia/Perth",
+ "Australia/Sydney"
+ )
+ assertTrue(zoneIds.containsAll(actualZoneIdStrings), "Missing: ${actualZoneIdStrings.minus(zoneIds.toSet())}")
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/mapper/EuropeZoneIdToZoneMapperTest.kt b/composeApp/src/commonTest/kotlin/mapper/EuropeZoneIdToZoneMapperTest.kt
new file mode 100644
index 0000000..0e1876e
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/mapper/EuropeZoneIdToZoneMapperTest.kt
@@ -0,0 +1,83 @@
+package mapper
+
+import data.kairos.model.Region
+import data.kairos.KairosRepository
+import org.koin.core.component.inject
+import org.koin.core.context.startKoin
+import org.koin.core.context.stopKoin
+import org.koin.test.KoinTest
+import repositoryModules
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+class EuropeZoneIdToZoneMapperTest : KoinTest {
+
+ private val kairosRepository: KairosRepository by inject()
+
+ private val zoneIds by lazy {
+ kairosRepository.zones
+ .filter { it.region == Region.Europe }
+ .map { it.timeZone.toString() }
+ }
+
+ @BeforeTest
+ fun setup() {
+ startKoin { modules(repositoryModules) }
+ }
+
+ @AfterTest
+ fun tearDown() {
+ stopKoin()
+ }
+
+ @Test
+ fun `Given Europe zoneIdStrings When mapping Then it should contain them`() {
+ val actualZoneIdStrings = listOf(
+ "Europe/Andorra",
+ "Europe/Vienna",
+ "Europe/Brussels",
+ "Europe/Sofia",
+ "Europe/Zurich",
+ "Europe/Prague",
+ "Europe/Berlin",
+ "Europe/Copenhagen",
+ "Europe/Madrid",
+ "Europe/Helsinki",
+ "Europe/Paris",
+ "Europe/London",
+ "Europe/Dublin",
+ "Europe/Isle_of_Man",
+ "Europe/Athens",
+ "Europe/Budapest",
+ "Europe/Rome",
+ "Europe/Jersey",
+ "Europe/Vilnius",
+ "Europe/Luxembourg",
+ "Europe/Riga",
+ "Europe/Monaco",
+ "Europe/Chisinau",
+ "Europe/Malta",
+ "Europe/Amsterdam",
+ "Europe/Oslo",
+ "Europe/Warsaw",
+ "Europe/Lisbon",
+ "Europe/Bucharest",
+ "Europe/San_Marino",
+ "Europe/Stockholm",
+ "Europe/Vatican",
+ "Europe/Sarajevo",
+ "Europe/Skopje",
+ "Europe/Zagreb",
+ "Europe/Tallinn",
+ "Europe/Tirane",
+ "Europe/Madrid",
+ "Europe/Belgrade",
+ "Europe/Bratislava",
+ "Europe/Ljubljana",
+ "Europe/Copenhagen"
+ )
+ assertTrue(zoneIds.containsAll(actualZoneIdStrings), "Missing: ${actualZoneIdStrings.minus(zoneIds.toSet())}")
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/Platform.jvm.kt b/composeApp/src/desktopMain/kotlin/Platform.jvm.kt
deleted file mode 100644
index f5e7e49..0000000
--- a/composeApp/src/desktopMain/kotlin/Platform.jvm.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-class JVMPlatform: Platform {
- override val name: String = "Java ${System.getProperty("java.version")}"
-}
-
-actual fun getPlatform(): Platform = JVMPlatform()
\ No newline at end of file
diff --git a/composeApp/src/desktopMain/kotlin/main.kt b/composeApp/src/desktopMain/kotlin/main.kt
deleted file mode 100644
index 5930e3a..0000000
--- a/composeApp/src/desktopMain/kotlin/main.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.window.Window
-import androidx.compose.ui.window.application
-
-fun main() = application {
- Window(onCloseRequest = ::exitApplication, title = "Twilight") {
- App()
- }
-}
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/Koin.kt b/composeApp/src/iosMain/kotlin/Koin.kt
new file mode 100644
index 0000000..8798fef
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/Koin.kt
@@ -0,0 +1,17 @@
+import di.mapperModule
+import di.repositoryModule
+import di.serviceModule
+import di.viewModelModule
+import org.koin.core.context.startKoin
+
+fun initKoin() {
+ startKoin {
+ modules(
+ platformModule(),
+ viewModelModule(),
+ serviceModule(),
+ repositoryModule(),
+ mapperModule()
+ )
+ }
+}
diff --git a/composeApp/src/iosMain/kotlin/MainViewController.kt b/composeApp/src/iosMain/kotlin/MainViewController.kt
index fa143d4..b621ce4 100644
--- a/composeApp/src/iosMain/kotlin/MainViewController.kt
+++ b/composeApp/src/iosMain/kotlin/MainViewController.kt
@@ -1,3 +1,19 @@
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.displayCutout
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.Scaffold
+import androidx.compose.ui.Modifier
import androidx.compose.ui.window.ComposeUIViewController
+import ui.theme.AppTheme
-fun MainViewController() = ComposeUIViewController { App() }
\ No newline at end of file
+fun MainViewController() = ComposeUIViewController {
+ AppTheme {
+ Scaffold {
+ val insetModifier = Modifier
+ .windowInsetsPadding(WindowInsets.displayCutout)
+ .consumeWindowInsets(it)
+ App(insetModifier)
+ }
+ }
+}
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/Platform.ios.kt b/composeApp/src/iosMain/kotlin/Platform.ios.kt
deleted file mode 100644
index 5cef987..0000000
--- a/composeApp/src/iosMain/kotlin/Platform.ios.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-import platform.UIKit.UIDevice
-
-class IOSPlatform: Platform {
- override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
-}
-
-actual fun getPlatform(): Platform = IOSPlatform()
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/expect.ios.kt b/composeApp/src/iosMain/kotlin/expect.ios.kt
new file mode 100644
index 0000000..4bbc3b6
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/expect.ios.kt
@@ -0,0 +1,48 @@
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import app.cash.sqldelight.driver.native.NativeSqliteDriver
+import com.delacrixmorgan.twilight.TwilightDatabase
+import data.utils.LocalDataStore
+import di.TwilightDatabaseWrapper
+import kotlinx.cinterop.ExperimentalForeignApi
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+import platform.Foundation.NSBundle
+import platform.Foundation.NSDocumentDirectory
+import platform.Foundation.NSFileManager
+import platform.Foundation.NSURL
+import platform.Foundation.NSUUID
+import platform.Foundation.NSUserDomainMask
+
+actual fun platformModule() = module {
+ single(named(LocalDataStore.Preferences.name)) { dataStore(LocalDataStore.Preferences.path()) }
+ single(named(LocalDataStore.CreateNewLocation.name)) { dataStore(LocalDataStore.CreateNewLocation.path()) }
+ single {
+ val driver = NativeSqliteDriver(TwilightDatabase.Schema, "twilight.db")
+ TwilightDatabaseWrapper(TwilightDatabase(driver))
+ }
+}
+
+@OptIn(ExperimentalForeignApi::class)
+fun dataStore(path: String): DataStore = createDataStore(
+ producePath = {
+ val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory(
+ directory = NSDocumentDirectory,
+ inDomain = NSUserDomainMask,
+ appropriateForURL = null,
+ create = false,
+ error = null,
+ )
+ requireNotNull(documentDirectory).path + "/$path"
+ }
+)
+
+actual fun randomUUID(): String = NSUUID().UUIDString()
+
+actual fun getVersionCode(): String {
+ return NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleVersion") as? String ?: "Unknown"
+}
+
+actual fun getVersionName(): String {
+ return NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String ?: "Unknown"
+}
\ No newline at end of file
diff --git a/composeApp/src/wasmJsMain/kotlin/Platform.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/Platform.wasmJs.kt
deleted file mode 100644
index 57b2e11..0000000
--- a/composeApp/src/wasmJsMain/kotlin/Platform.wasmJs.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-class WasmPlatform: Platform {
- override val name: String = "Web with Kotlin/Wasm"
-}
-
-actual fun getPlatform(): Platform = WasmPlatform()
\ No newline at end of file
diff --git a/composeApp/src/wasmJsMain/kotlin/main.kt b/composeApp/src/wasmJsMain/kotlin/main.kt
deleted file mode 100644
index b9c0f64..0000000
--- a/composeApp/src/wasmJsMain/kotlin/main.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.window.CanvasBasedWindow
-
-@OptIn(ExperimentalComposeUiApi::class)
-fun main() {
- CanvasBasedWindow(canvasElementId = "ComposeTarget") { App() }
-}
\ No newline at end of file
diff --git a/composeApp/src/wasmJsMain/resources/index.html b/composeApp/src/wasmJsMain/resources/index.html
deleted file mode 100644
index 9ba000f..0000000
--- a/composeApp/src/wasmJsMain/resources/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- Compose App
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 89e6b68..389afa8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,19 +1,11 @@
kotlin.code.style=official
-
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
-
-
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true
-
-#Compose
-org.jetbrains.compose.experimental.wasm.enabled=true
-
#MPP
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.enableCInteropCommonization=true
-
#Development
development=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 09abc26..cf0e99c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,36 +1,47 @@
[versions]
-agp = "8.2.2"
-android-compileSdk = "34"
+agp = "8.5.2" # https://mvnrepository.com/artifact/com.android.application/com.android.application.gradle.plugin
+android-compileSdk = "35"
android-minSdk = "24"
-android-targetSdk = "34"
-androidx-activityCompose = "1.8.2"
-androidx-appcompat = "1.6.1"
-androidx-constraintlayout = "2.1.4"
-androidx-core-ktx = "1.12.0"
-androidx-espresso-core = "3.5.1"
-androidx-material = "1.11.0"
-androidx-test-junit = "1.1.5"
-compose = "1.6.5"
-compose-plugin = "1.6.1"
-junit = "4.13.2"
-kotlin = "1.9.22"
+android-targetSdk = "35"
+kotlin = "2.0.10"
+compose-plugin = "1.6.10"
+kotlinx-serialization-json = "1.6.3"
+kotlinx-datetime = "0.6.0"
+androidx-navigation-compose = "2.8.0-alpha08" # https://mvnrepository.com/artifact/org.jetbrains.androidx.navigation/navigation-compose-uikitarm64
+androidx-lifecycle = "2.8.4"
+androidx-datastore-preference = "1.1.1"
+composeBoM = "2024.08.00"
+koinBoM = "3.5.6"
+koinComposeMultiplatform = "4.0.0-RC1" # https://central.sonatype.com/artifact/io.insert-koin/koin-compose-viewmodel
+sqlDelight = "2.0.2"
+kermit = "2.0.3"
[libraries]
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
+kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
+androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation-compose" }
+androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
+androidx-datastore-preference = { module = "androidx.datastore:datastore-preferences-core", version.ref = "androidx-datastore-preference" }
+compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBoM" }
+compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+compose-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "compose-plugin" }
+koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBoM" }
+koin-core = { module = "io.insert-koin:koin-core" }
+koin-android = { module = "io.insert-koin:koin-android" }
+koin-composeVM = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
-kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
-junit = { group = "junit", name = "junit", version.ref = "junit" }
-androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
-androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" }
-androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
-androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
-androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" }
-androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
-androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
+koin-test = { module = "io.insert-koin:koin-test", version.ref = "koinBoM" }
+sqldelight-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" }
+sqldelight-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" }
+sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" }
+kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
-kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
\ No newline at end of file
+composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+sqlDelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 033e24c..2c35211 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23..9355b41 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index fcb6fca..f5feea6 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,7 +85,9 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC3045
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -201,11 +205,11 @@ fi
# 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"'
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
diff --git a/gradlew.bat b/gradlew.bat
index 6689b85..9b42019 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 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.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ 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.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj
index 5b57af0..6c7c136 100644
--- a/iosApp/iosApp.xcodeproj/project.pbxproj
+++ b/iosApp/iosApp.xcodeproj/project.pbxproj
@@ -17,7 +17,7 @@
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; };
- 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7555FF7B242A565900829871 /* Twilight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Twilight.app; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; };
@@ -62,7 +62,7 @@
7555FF7C242A565900829871 /* Products */ = {
isa = PBXGroup;
children = (
- 7555FF7B242A565900829871 /* iosApp.app */,
+ 7555FF7B242A565900829871 /* Twilight.app */,
);
name = Products;
sourceTree = "";
@@ -107,7 +107,7 @@
packageProductDependencies = (
);
productName = iosApp;
- productReference = 7555FF7B242A565900829871 /* iosApp.app */;
+ productReference = 7555FF7B242A565900829871 /* Twilight.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -316,7 +316,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = "${TEAM_ID}";
+ DEVELOPMENT_TEAM = R9PZNF3QK4;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
@@ -331,6 +331,7 @@
"$(inherited)",
"-framework",
composeApp,
+ "-lsqlite3",
);
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
PRODUCT_NAME = "${APP_NAME}";
@@ -347,7 +348,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
- DEVELOPMENT_TEAM = "${TEAM_ID}";
+ DEVELOPMENT_TEAM = R9PZNF3QK4;
ENABLE_PREVIEWS = YES;
FRAMEWORK_SEARCH_PATHS = (
"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)",
@@ -362,6 +363,7 @@
"$(inherited)",
"-framework",
composeApp,
+ "-lsqlite3",
);
PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}";
PRODUCT_NAME = "${APP_NAME}";
diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
index 8edf56e..3f0ce92 100644
--- a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,7 +1,7 @@
{
"images" : [
{
- "filename" : "app-icon-1024.png",
+ "filename" : "app-icon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
deleted file mode 100644
index 53fc536..0000000
Binary files a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png and /dev/null differ
diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon.png
new file mode 100644
index 0000000..817d8bf
Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon.png differ
diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json
index 4aa7c53..73c0059 100644
--- a/iosApp/iosApp/Assets.xcassets/Contents.json
+++ b/iosApp/iosApp/Assets.xcassets/Contents.json
@@ -3,4 +3,4 @@
"author" : "xcode",
"version" : 1
}
-}
\ No newline at end of file
+}
diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift
index 3cd5c32..9241814 100644
--- a/iosApp/iosApp/ContentView.swift
+++ b/iosApp/iosApp/ContentView.swift
@@ -12,10 +12,6 @@ struct ComposeView: UIViewControllerRepresentable {
struct ContentView: View {
var body: some View {
- ComposeView()
- .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
+ ComposeView().ignoresSafeArea()
}
}
-
-
-
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index 412e378..a4f0c52 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 1.0
+ 2024.1
CFBundleVersion
1
LSRequiresIPhoneOS
diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift
index 0648e86..75345c2 100644
--- a/iosApp/iosApp/iOSApp.swift
+++ b/iosApp/iosApp/iOSApp.swift
@@ -1,10 +1,16 @@
import SwiftUI
+import ComposeApp
@main
struct iOSApp: App {
+
+ init() {
+ KoinKt.doInitKoin()
+ }
+
var body: some Scene {
WindowGroup {
- ContentView()
+ ContentView()
}
}
-}
\ No newline at end of file
+}
diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock
new file mode 100644
index 0000000..edacd2c
--- /dev/null
+++ b/kotlin-js-store/yarn.lock
@@ -0,0 +1,2831 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@colors/colors@1.5.0":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
+ integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
+
+"@discoveryjs/json-ext@^0.5.0":
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
+ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
+
+"@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
+ integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==
+ dependencies:
+ "@jridgewell/set-array" "^1.2.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/set-array@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
+ integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
+
+"@jridgewell/source-map@^0.3.3":
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a"
+ integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.25"
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+ version "0.3.25"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
+ integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@leichtgewicht/ip-codec@^2.0.1":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1"
+ integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==
+
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
+ integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
+"@types/body-parser@*":
+ version "1.19.5"
+ resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
+ integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
+ dependencies:
+ "@types/connect" "*"
+ "@types/node" "*"
+
+"@types/bonjour@^3.5.9":
+ version "3.5.13"
+ resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956"
+ integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/connect-history-api-fallback@^1.3.5":
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3"
+ integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==
+ dependencies:
+ "@types/express-serve-static-core" "*"
+ "@types/node" "*"
+
+"@types/connect@*":
+ version "3.4.38"
+ resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
+ integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
+ dependencies:
+ "@types/node" "*"
+
+"@types/cookie@^0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
+ integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
+
+"@types/cors@^2.8.12":
+ version "2.8.17"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
+ integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==
+ dependencies:
+ "@types/node" "*"
+
+"@types/eslint-scope@^3.7.3":
+ version "3.7.7"
+ resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5"
+ integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==
+ dependencies:
+ "@types/eslint" "*"
+ "@types/estree" "*"
+
+"@types/eslint@*":
+ version "8.56.7"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.7.tgz#c33b5b5a9cfb66881beb7b5be6c34aa3e81d3366"
+ integrity sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==
+ dependencies:
+ "@types/estree" "*"
+ "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^1.0.0":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
+ integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
+
+"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33":
+ version "4.19.0"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz#3ae8ab3767d98d0b682cda063c3339e1e86ccfaa"
+ integrity sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==
+ dependencies:
+ "@types/node" "*"
+ "@types/qs" "*"
+ "@types/range-parser" "*"
+ "@types/send" "*"
+
+"@types/express@*", "@types/express@^4.17.13":
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
+ integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
+ dependencies:
+ "@types/body-parser" "*"
+ "@types/express-serve-static-core" "^4.17.33"
+ "@types/qs" "*"
+ "@types/serve-static" "*"
+
+"@types/http-errors@*":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
+ integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
+
+"@types/http-proxy@^1.17.8":
+ version "1.17.14"
+ resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec"
+ integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==
+ dependencies:
+ "@types/node" "*"
+
+"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
+ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
+"@types/mime@^1":
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
+ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
+
+"@types/node-forge@^1.3.0":
+ version "1.3.11"
+ resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da"
+ integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*", "@types/node@>=10.0.0":
+ version "20.12.5"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3"
+ integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==
+ dependencies:
+ undici-types "~5.26.4"
+
+"@types/qs@*":
+ version "6.9.14"
+ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b"
+ integrity sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==
+
+"@types/range-parser@*":
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
+ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
+
+"@types/retry@0.12.0":
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
+ integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
+
+"@types/send@*":
+ version "0.17.4"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
+ integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
+ dependencies:
+ "@types/mime" "^1"
+ "@types/node" "*"
+
+"@types/serve-index@^1.9.1":
+ version "1.9.4"
+ resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898"
+ integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==
+ dependencies:
+ "@types/express" "*"
+
+"@types/serve-static@*", "@types/serve-static@^1.13.10":
+ version "1.15.7"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714"
+ integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==
+ dependencies:
+ "@types/http-errors" "*"
+ "@types/node" "*"
+ "@types/send" "*"
+
+"@types/sockjs@^0.3.33":
+ version "0.3.36"
+ resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535"
+ integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==
+ dependencies:
+ "@types/node" "*"
+
+"@types/ws@^8.5.1":
+ version "8.5.10"
+ resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787"
+ integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==
+ dependencies:
+ "@types/node" "*"
+
+"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.11.5":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb"
+ integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==
+ dependencies:
+ "@webassemblyjs/helper-numbers" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431"
+ integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==
+
+"@webassemblyjs/helper-api-error@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768"
+ integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==
+
+"@webassemblyjs/helper-buffer@1.12.1":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6"
+ integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==
+
+"@webassemblyjs/helper-numbers@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5"
+ integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==
+ dependencies:
+ "@webassemblyjs/floating-point-hex-parser" "1.11.6"
+ "@webassemblyjs/helper-api-error" "1.11.6"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9"
+ integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==
+
+"@webassemblyjs/helper-wasm-section@1.12.1":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf"
+ integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==
+ dependencies:
+ "@webassemblyjs/ast" "1.12.1"
+ "@webassemblyjs/helper-buffer" "1.12.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/wasm-gen" "1.12.1"
+
+"@webassemblyjs/ieee754@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a"
+ integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7"
+ integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.6":
+ version "1.11.6"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a"
+ integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==
+
+"@webassemblyjs/wasm-edit@^1.11.5":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b"
+ integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==
+ dependencies:
+ "@webassemblyjs/ast" "1.12.1"
+ "@webassemblyjs/helper-buffer" "1.12.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/helper-wasm-section" "1.12.1"
+ "@webassemblyjs/wasm-gen" "1.12.1"
+ "@webassemblyjs/wasm-opt" "1.12.1"
+ "@webassemblyjs/wasm-parser" "1.12.1"
+ "@webassemblyjs/wast-printer" "1.12.1"
+
+"@webassemblyjs/wasm-gen@1.12.1":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547"
+ integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==
+ dependencies:
+ "@webassemblyjs/ast" "1.12.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
+
+"@webassemblyjs/wasm-opt@1.12.1":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5"
+ integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==
+ dependencies:
+ "@webassemblyjs/ast" "1.12.1"
+ "@webassemblyjs/helper-buffer" "1.12.1"
+ "@webassemblyjs/wasm-gen" "1.12.1"
+ "@webassemblyjs/wasm-parser" "1.12.1"
+
+"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.11.5":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937"
+ integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==
+ dependencies:
+ "@webassemblyjs/ast" "1.12.1"
+ "@webassemblyjs/helper-api-error" "1.11.6"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.6"
+ "@webassemblyjs/ieee754" "1.11.6"
+ "@webassemblyjs/leb128" "1.11.6"
+ "@webassemblyjs/utf8" "1.11.6"
+
+"@webassemblyjs/wast-printer@1.12.1":
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac"
+ integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==
+ dependencies:
+ "@webassemblyjs/ast" "1.12.1"
+ "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^2.1.0":
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646"
+ integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==
+
+"@webpack-cli/info@^2.0.1":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd"
+ integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==
+
+"@webpack-cli/serve@^2.0.3":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e"
+ integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+abab@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
+ integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
+
+accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
+ integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+ dependencies:
+ mime-types "~2.1.34"
+ negotiator "0.6.3"
+
+acorn-import-assertions@^1.7.6:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac"
+ integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==
+
+acorn@^8.7.1, acorn@^8.8.2:
+ version "8.11.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
+ integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
+
+ajv-formats@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
+ integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
+ dependencies:
+ ajv "^8.0.0"
+
+ajv-keywords@^3.5.2:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+ integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv-keywords@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16"
+ integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+
+ajv@^6.12.5:
+ version "6.12.6"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ajv@^8.0.0, ajv@^8.9.0:
+ version "8.12.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1"
+ integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==
+ dependencies:
+ fast-deep-equal "^3.1.1"
+ json-schema-traverse "^1.0.0"
+ require-from-string "^2.0.2"
+ uri-js "^4.2.2"
+
+ansi-colors@4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+ integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
+ansi-html-community@^0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
+ integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base64id@2.0.0, base64id@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
+ integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
+
+batch@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
+ integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==
+
+binary-extensions@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
+ integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
+
+body-parser@1.20.2, body-parser@^1.19.0:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
+ integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
+ dependencies:
+ bytes "3.1.2"
+ content-type "~1.0.5"
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ on-finished "2.4.1"
+ qs "6.11.0"
+ raw-body "2.5.2"
+ type-is "~1.6.18"
+ unpipe "1.0.0"
+
+bonjour-service@^1.0.11:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02"
+ integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==
+ dependencies:
+ fast-deep-equal "^3.1.3"
+ multicast-dns "^7.2.5"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+ integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+ dependencies:
+ balanced-match "^1.0.0"
+
+braces@^3.0.2, braces@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+ integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+ dependencies:
+ fill-range "^7.0.1"
+
+browser-stdout@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
+ integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
+
+browserslist@^4.14.5:
+ version "4.23.0"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab"
+ integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==
+ dependencies:
+ caniuse-lite "^1.0.30001587"
+ electron-to-chromium "^1.4.668"
+ node-releases "^2.0.14"
+ update-browserslist-db "^1.0.13"
+
+buffer-from@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+ integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+ integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==
+
+bytes@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+ integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+call-bind@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
+ integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ set-function-length "^1.2.1"
+
+camelcase@^6.0.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
+ integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+
+caniuse-lite@^1.0.30001587:
+ version "1.0.30001606"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e"
+ integrity sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg==
+
+chalk@^4.1.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+chokidar@3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+ integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+chokidar@^3.5.1, chokidar@^3.5.3:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
+ integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+chrome-trace-event@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+ integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+cliui@^7.0.2:
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+ integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.0"
+ wrap-ansi "^7.0.0"
+
+clone-deep@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+ integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+ dependencies:
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.2"
+ shallow-clone "^3.0.0"
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+colorette@^2.0.10, colorette@^2.0.14:
+ version "2.0.20"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
+ integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
+
+commander@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
+ integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
+
+commander@^2.20.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+compressible@~2.0.16:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+ integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+ dependencies:
+ mime-db ">= 1.43.0 < 2"
+
+compression@^1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+ integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+ dependencies:
+ accepts "~1.3.5"
+ bytes "3.0.0"
+ compressible "~2.0.16"
+ debug "2.6.9"
+ on-headers "~1.0.2"
+ safe-buffer "5.1.2"
+ vary "~1.1.2"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+connect-history-api-fallback@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8"
+ integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==
+
+connect@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
+ integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
+ dependencies:
+ debug "2.6.9"
+ finalhandler "1.1.2"
+ parseurl "~1.3.3"
+ utils-merge "1.0.1"
+
+content-disposition@0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+ integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+ dependencies:
+ safe-buffer "5.2.1"
+
+content-type@~1.0.4, content-type@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
+ integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+
+cookie@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
+
+cookie@~0.4.1:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
+ integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
+
+core-util-is@~1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+cors@~2.8.5:
+ version "2.8.5"
+ resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
+ integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+ dependencies:
+ object-assign "^4"
+ vary "^1"
+
+cross-spawn@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+custom-event@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+ integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==
+
+date-format@^4.0.14:
+ version "4.0.14"
+ resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400"
+ integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==
+
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@4.3.4, debug@^4.1.0, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
+decamelize@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837"
+ integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
+
+default-gateway@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71"
+ integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==
+ dependencies:
+ execa "^5.0.0"
+
+define-data-property@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
+ integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
+ dependencies:
+ es-define-property "^1.0.0"
+ es-errors "^1.3.0"
+ gopd "^1.0.1"
+
+define-lazy-prop@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
+ integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
+
+depd@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
+
+destroy@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
+ integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+
+detect-node@^2.0.4:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
+ integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
+
+di@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+ integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==
+
+diff@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
+ integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+
+dns-packet@^5.2.2:
+ version "5.6.1"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f"
+ integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==
+ dependencies:
+ "@leichtgewicht/ip-codec" "^2.0.1"
+
+dom-serialize@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
+ integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==
+ dependencies:
+ custom-event "~1.0.0"
+ ent "~2.2.0"
+ extend "^3.0.0"
+ void-elements "^2.0.0"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
+
+electron-to-chromium@^1.4.668:
+ version "1.4.729"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz#8477d21e2a50993781950885b2731d92ad532c00"
+ integrity sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+
+engine.io-parser@~5.2.1:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49"
+ integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==
+
+engine.io@~6.5.2:
+ version "6.5.4"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.4.tgz#6822debf324e781add2254e912f8568508850cdc"
+ integrity sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==
+ dependencies:
+ "@types/cookie" "^0.4.1"
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
+ accepts "~1.3.4"
+ base64id "2.0.0"
+ cookie "~0.4.1"
+ cors "~2.8.5"
+ debug "~4.3.1"
+ engine.io-parser "~5.2.1"
+ ws "~8.11.0"
+
+enhanced-resolve@^5.13.0:
+ version "5.16.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787"
+ integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
+ent@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+ integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==
+
+envinfo@^7.7.3:
+ version "7.12.0"
+ resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.12.0.tgz#b56723b39c2053d67ea5714f026d05d4f5cc7acd"
+ integrity sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg==
+
+es-define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
+ integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
+ dependencies:
+ get-intrinsic "^1.2.4"
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-module-lexer@^1.2.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.0.tgz#4878fee3789ad99e065f975fdd3c645529ff0236"
+ integrity sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==
+
+escalade@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
+ integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+escape-string-regexp@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-scope@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+ integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^4.1.1"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+ integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
+
+eventemitter3@^4.0.0:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
+events@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+execa@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+ integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+ dependencies:
+ cross-spawn "^7.0.3"
+ get-stream "^6.0.0"
+ human-signals "^2.1.0"
+ is-stream "^2.0.0"
+ merge-stream "^2.0.0"
+ npm-run-path "^4.0.1"
+ onetime "^5.1.2"
+ signal-exit "^3.0.3"
+ strip-final-newline "^2.0.0"
+
+express@^4.17.3:
+ version "4.19.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
+ integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
+ dependencies:
+ accepts "~1.3.8"
+ array-flatten "1.1.1"
+ body-parser "1.20.2"
+ content-disposition "0.5.4"
+ content-type "~1.0.4"
+ cookie "0.6.0"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "2.0.0"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "1.2.0"
+ fresh "0.5.2"
+ http-errors "2.0.0"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "2.4.1"
+ parseurl "~1.3.3"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.7"
+ qs "6.11.0"
+ range-parser "~1.2.1"
+ safe-buffer "5.2.1"
+ send "0.18.0"
+ serve-static "1.15.0"
+ setprototypeof "1.2.0"
+ statuses "2.0.1"
+ type-is "~1.6.18"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+extend@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fastest-levenshtein@^1.0.12:
+ version "1.0.16"
+ resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5"
+ integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==
+
+faye-websocket@^0.11.3:
+ version "0.11.4"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
+ integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+fill-range@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+ integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+finalhandler@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
+ integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ statuses "~1.5.0"
+ unpipe "~1.0.0"
+
+finalhandler@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
+ integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "2.4.1"
+ parseurl "~1.3.3"
+ statuses "2.0.1"
+ unpipe "~1.0.0"
+
+find-up@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+find-up@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+flat@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
+ integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
+
+flatted@^3.2.7:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
+ integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
+
+follow-redirects@^1.0.0:
+ version "1.15.6"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+ integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
+
+format-util@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271"
+ integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg==
+
+forwarded@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+ integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
+
+fs-extra@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+ integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
+fs-monkey@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788"
+ integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
+ integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ has-proto "^1.0.1"
+ has-symbols "^1.0.3"
+ hasown "^2.0.0"
+
+get-stream@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+ integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
+glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-to-regexp@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+ integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+glob@7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+ integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.1.3, glob@^7.1.7:
+ version "7.2.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
+ integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.1.1"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+gopd@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+ integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+ dependencies:
+ get-intrinsic "^1.1.3"
+
+graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+handle-thing@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
+ integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-property-descriptors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
+ integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
+ dependencies:
+ es-define-property "^1.0.0"
+
+has-proto@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
+ integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
+
+has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
+hasown@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+he@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+ integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+hpack.js@^2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
+ integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==
+ dependencies:
+ inherits "^2.0.1"
+ obuf "^1.0.0"
+ readable-stream "^2.0.1"
+ wbuf "^1.1.0"
+
+html-entities@^2.3.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f"
+ integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==
+
+http-deceiver@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
+ integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==
+
+http-errors@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
+ integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
+ dependencies:
+ depd "2.0.0"
+ inherits "2.0.4"
+ setprototypeof "1.2.0"
+ statuses "2.0.1"
+ toidentifier "1.0.1"
+
+http-errors@~1.6.2:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+ integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.0"
+ statuses ">= 1.4.0 < 2"
+
+http-parser-js@>=0.5.1:
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
+ integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==
+
+http-proxy-middleware@^2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
+ integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
+ dependencies:
+ "@types/http-proxy" "^1.17.8"
+ http-proxy "^1.18.1"
+ is-glob "^4.0.1"
+ is-plain-obj "^3.0.0"
+ micromatch "^4.0.2"
+
+http-proxy@^1.18.1:
+ version "1.18.1"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
+ integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
+ dependencies:
+ eventemitter3 "^4.0.0"
+ follow-redirects "^1.0.0"
+ requires-port "^1.0.0"
+
+human-signals@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+ integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+iconv-lite@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+import-local@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4"
+ integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==
+ dependencies:
+ pkg-dir "^4.2.0"
+ resolve-cwd "^3.0.0"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inherits@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
+
+interpret@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
+ integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
+
+ipaddr.js@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+ipaddr.js@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f"
+ integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-core-module@^2.13.0:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
+ integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
+ dependencies:
+ hasown "^2.0.0"
+
+is-docker@^2.0.0, is-docker@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+ integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-obj@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+ integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
+is-plain-obj@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
+ integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
+
+is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+is-stream@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+ integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+is-unicode-supported@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
+ integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
+
+is-wsl@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+ integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+ dependencies:
+ is-docker "^2.0.0"
+
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
+isbinaryfile@^4.0.8:
+ version "4.0.10"
+ resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3"
+ integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
+
+jest-worker@^27.4.5:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
+ integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
+ dependencies:
+ "@types/node" "*"
+ merge-stream "^2.0.0"
+ supports-color "^8.0.0"
+
+js-yaml@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+json-parse-even-better-errors@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+ integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema-traverse@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+ integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+jsonfile@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+ integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
+karma-chrome-launcher@3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9"
+ integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==
+ dependencies:
+ which "^1.2.1"
+
+karma-mocha@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d"
+ integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==
+ dependencies:
+ minimist "^1.2.3"
+
+karma-sourcemap-loader@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.4.0.tgz#b01d73f8f688f533bcc8f5d273d43458e13b5488"
+ integrity sha512-xCRL3/pmhAYF3I6qOrcn0uhbQevitc2DERMPH82FMnG+4WReoGcGFZb1pURf2a5apyrOHRdvD+O6K7NljqKHyA==
+ dependencies:
+ graceful-fs "^4.2.10"
+
+karma-webpack@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-5.0.0.tgz#2a2c7b80163fe7ffd1010f83f5507f95ef39f840"
+ integrity sha512-+54i/cd3/piZuP3dr54+NcFeKOPnys5QeM1IY+0SPASwrtHsliXUiCL50iW+K9WWA7RvamC4macvvQ86l3KtaA==
+ dependencies:
+ glob "^7.1.3"
+ minimatch "^3.0.4"
+ webpack-merge "^4.1.5"
+
+karma@6.4.2:
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.2.tgz#a983f874cee6f35990c4b2dcc3d274653714de8e"
+ integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==
+ dependencies:
+ "@colors/colors" "1.5.0"
+ body-parser "^1.19.0"
+ braces "^3.0.2"
+ chokidar "^3.5.1"
+ connect "^3.7.0"
+ di "^0.0.1"
+ dom-serialize "^2.2.1"
+ glob "^7.1.7"
+ graceful-fs "^4.2.6"
+ http-proxy "^1.18.1"
+ isbinaryfile "^4.0.8"
+ lodash "^4.17.21"
+ log4js "^6.4.1"
+ mime "^2.5.2"
+ minimatch "^3.0.4"
+ mkdirp "^0.5.5"
+ qjobs "^1.2.0"
+ range-parser "^1.2.1"
+ rimraf "^3.0.2"
+ socket.io "^4.4.1"
+ source-map "^0.6.1"
+ tmp "^0.2.1"
+ ua-parser-js "^0.7.30"
+ yargs "^16.1.1"
+
+kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+launch-editor@^2.6.0:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c"
+ integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==
+ dependencies:
+ picocolors "^1.0.0"
+ shell-quote "^1.8.1"
+
+loader-runner@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
+ integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash@^4.17.15, lodash@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+log-symbols@4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
+ integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
+ dependencies:
+ chalk "^4.1.0"
+ is-unicode-supported "^0.1.0"
+
+log4js@^6.4.1:
+ version "6.9.1"
+ resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6"
+ integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==
+ dependencies:
+ date-format "^4.0.14"
+ debug "^4.3.4"
+ flatted "^3.2.7"
+ rfdc "^1.3.0"
+ streamroller "^3.1.5"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
+
+memfs@^3.4.3:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6"
+ integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ==
+ dependencies:
+ fs-monkey "^1.0.4"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+ integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+
+micromatch@^4.0.2:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+ integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+ dependencies:
+ braces "^3.0.2"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+mime@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mime@^2.5.2:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
+ integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
+
+mimic-fn@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+ integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+minimalistic-assert@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimatch@5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b"
+ integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+minimatch@^3.0.4, minimatch@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@^1.2.3, minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+mkdirp@^0.5.5:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+ integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+ dependencies:
+ minimist "^1.2.6"
+
+mocha@10.2.0:
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8"
+ integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==
+ dependencies:
+ ansi-colors "4.1.1"
+ browser-stdout "1.3.1"
+ chokidar "3.5.3"
+ debug "4.3.4"
+ diff "5.0.0"
+ escape-string-regexp "4.0.0"
+ find-up "5.0.0"
+ glob "7.2.0"
+ he "1.2.0"
+ js-yaml "4.1.0"
+ log-symbols "4.1.0"
+ minimatch "5.0.1"
+ ms "2.1.3"
+ nanoid "3.3.3"
+ serialize-javascript "6.0.0"
+ strip-json-comments "3.1.1"
+ supports-color "8.1.1"
+ workerpool "6.2.1"
+ yargs "16.2.0"
+ yargs-parser "20.2.4"
+ yargs-unparser "2.0.0"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+multicast-dns@^7.2.5:
+ version "7.2.5"
+ resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced"
+ integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==
+ dependencies:
+ dns-packet "^5.2.2"
+ thunky "^1.0.2"
+
+nanoid@3.3.3:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
+ integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
+
+negotiator@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+ integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
+neo-async@^2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+ integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-forge@^1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
+ integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
+
+node-releases@^2.0.14:
+ version "2.0.14"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
+ integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+npm-run-path@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+ integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+ dependencies:
+ path-key "^3.0.0"
+
+object-assign@^4:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-inspect@^1.13.1:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
+ integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
+
+obuf@^1.0.0, obuf@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
+ integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
+
+on-finished@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
+ integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
+ dependencies:
+ ee-first "1.1.1"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+ integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
+once@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+onetime@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+ integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+ dependencies:
+ mimic-fn "^2.1.0"
+
+open@^8.0.9:
+ version "8.4.2"
+ resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"
+ integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==
+ dependencies:
+ define-lazy-prop "^2.0.0"
+ is-docker "^2.1.1"
+ is-wsl "^2.2.0"
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+p-retry@^4.5.0:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
+ integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
+ dependencies:
+ "@types/retry" "0.12.0"
+ retry "^0.13.1"
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+parseurl@~1.3.2, parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+ integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^3.0.0, path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+ integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pkg-dir@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+proxy-addr@~2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+ integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
+ dependencies:
+ forwarded "0.2.0"
+ ipaddr.js "1.9.1"
+
+punycode@^2.1.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+
+qjobs@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
+ integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
+
+qs@6.11.0:
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
+ integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
+ dependencies:
+ side-channel "^1.0.4"
+
+randombytes@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+ integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+range-parser@^1.2.1, range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+ integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
+ dependencies:
+ bytes "3.1.2"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+readable-stream@^2.0.1:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
+ integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.0.6:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
+ integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+rechoir@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
+ integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==
+ dependencies:
+ resolve "^1.20.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+require-from-string@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
+resolve-cwd@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+ integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+ dependencies:
+ resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+ integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.20.0:
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
+ dependencies:
+ is-core-module "^2.13.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+retry@^0.13.1:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
+ integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==
+
+rfdc@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f"
+ integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==
+
+rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
+safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+schema-utils@^3.1.1, schema-utils@^3.1.2:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe"
+ integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
+ dependencies:
+ "@types/json-schema" "^7.0.8"
+ ajv "^6.12.5"
+ ajv-keywords "^3.5.2"
+
+schema-utils@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b"
+ integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==
+ dependencies:
+ "@types/json-schema" "^7.0.9"
+ ajv "^8.9.0"
+ ajv-formats "^2.1.1"
+ ajv-keywords "^5.1.0"
+
+select-hose@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
+ integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==
+
+selfsigned@^2.1.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0"
+ integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==
+ dependencies:
+ "@types/node-forge" "^1.3.0"
+ node-forge "^1"
+
+send@0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
+ integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
+ dependencies:
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "2.0.0"
+ mime "1.6.0"
+ ms "2.1.3"
+ on-finished "2.4.1"
+ range-parser "~1.2.1"
+ statuses "2.0.1"
+
+serialize-javascript@6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+ integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+ dependencies:
+ randombytes "^2.1.0"
+
+serialize-javascript@^6.0.1:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
+ integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
+ dependencies:
+ randombytes "^2.1.0"
+
+serve-index@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
+ integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==
+ dependencies:
+ accepts "~1.3.4"
+ batch "0.6.1"
+ debug "2.6.9"
+ escape-html "~1.0.3"
+ http-errors "~1.6.2"
+ mime-types "~2.1.17"
+ parseurl "~1.3.2"
+
+serve-static@1.15.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
+ integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
+ dependencies:
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "0.18.0"
+
+set-function-length@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
+ integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
+ dependencies:
+ define-data-property "^1.1.4"
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+ get-intrinsic "^1.2.4"
+ gopd "^1.0.1"
+ has-property-descriptors "^1.0.2"
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+ integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+setprototypeof@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+ integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+shallow-clone@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+ integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+ dependencies:
+ kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+shell-quote@^1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
+ integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
+
+side-channel@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
+ integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
+ dependencies:
+ call-bind "^1.0.7"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.4"
+ object-inspect "^1.13.1"
+
+signal-exit@^3.0.3:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+socket.io-adapter@~2.5.2:
+ version "2.5.4"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz#4fdb1358667f6d68f25343353bd99bd11ee41006"
+ integrity sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==
+ dependencies:
+ debug "~4.3.4"
+ ws "~8.11.0"
+
+socket.io-parser@~4.2.4:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
+ integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
+socket.io@^4.4.1:
+ version "4.7.5"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.7.5.tgz#56eb2d976aef9d1445f373a62d781a41c7add8f8"
+ integrity sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==
+ dependencies:
+ accepts "~1.3.4"
+ base64id "~2.0.0"
+ cors "~2.8.5"
+ debug "~4.3.2"
+ engine.io "~6.5.2"
+ socket.io-adapter "~2.5.2"
+ socket.io-parser "~4.2.4"
+
+sockjs@^0.3.24:
+ version "0.3.24"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
+ integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==
+ dependencies:
+ faye-websocket "^0.11.3"
+ uuid "^8.3.2"
+ websocket-driver "^0.7.4"
+
+source-map-js@^1.0.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
+ integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
+
+source-map-loader@4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2"
+ integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==
+ dependencies:
+ abab "^2.0.6"
+ iconv-lite "^0.6.3"
+ source-map-js "^1.0.2"
+
+source-map-support@~0.5.20:
+ version "0.5.21"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+ integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdy-transport@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
+ integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==
+ dependencies:
+ debug "^4.1.0"
+ detect-node "^2.0.4"
+ hpack.js "^2.1.6"
+ obuf "^1.1.2"
+ readable-stream "^3.0.6"
+ wbuf "^1.7.3"
+
+spdy@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b"
+ integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==
+ dependencies:
+ debug "^4.1.0"
+ handle-thing "^2.0.0"
+ http-deceiver "^1.2.7"
+ select-hose "^2.0.0"
+ spdy-transport "^3.0.0"
+
+statuses@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
+ integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
+"statuses@>= 1.4.0 < 2", statuses@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+ integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
+
+streamroller@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff"
+ integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==
+ dependencies:
+ date-format "^4.0.14"
+ debug "^4.3.4"
+ fs-extra "^8.1.0"
+
+string-width@^4.1.0, string-width@^4.2.0:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-final-newline@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+ integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+strip-json-comments@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@8.1.1, supports-color@^8.0.0:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+ integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tapable@^2.1.1, tapable@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+ integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
+terser-webpack-plugin@^5.3.7:
+ version "5.3.10"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199"
+ integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==
+ dependencies:
+ "@jridgewell/trace-mapping" "^0.3.20"
+ jest-worker "^27.4.5"
+ schema-utils "^3.1.1"
+ serialize-javascript "^6.0.1"
+ terser "^5.26.0"
+
+terser@^5.26.0:
+ version "5.30.3"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2"
+ integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==
+ dependencies:
+ "@jridgewell/source-map" "^0.3.3"
+ acorn "^8.8.2"
+ commander "^2.20.0"
+ source-map-support "~0.5.20"
+
+thunky@^1.0.2:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
+ integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
+
+tmp@^0.2.1:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae"
+ integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+toidentifier@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+ integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
+type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+ integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.24"
+
+typescript@5.0.4:
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
+ integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
+
+ua-parser-js@^0.7.30:
+ version "0.7.37"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832"
+ integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==
+
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
+universalify@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+ integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
+
+update-browserslist-db@^1.0.13:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
+ integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==
+ dependencies:
+ escalade "^3.1.1"
+ picocolors "^1.0.0"
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
+
+uuid@^8.3.2:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
+vary@^1, vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
+void-elements@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+ integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==
+
+watchpack@^2.4.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff"
+ integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==
+ dependencies:
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.1.2"
+
+wbuf@^1.1.0, wbuf@^1.7.3:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
+ integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
+ dependencies:
+ minimalistic-assert "^1.0.0"
+
+webpack-cli@5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.0.tgz#abc4b1f44b50250f2632d8b8b536cfe2f6257891"
+ integrity sha512-a7KRJnCxejFoDpYTOwzm5o21ZXMaNqtRlvS183XzGDUPRdVEzJNImcQokqYZ8BNTnk9DkKiuWxw75+DCCoZ26w==
+ dependencies:
+ "@discoveryjs/json-ext" "^0.5.0"
+ "@webpack-cli/configtest" "^2.1.0"
+ "@webpack-cli/info" "^2.0.1"
+ "@webpack-cli/serve" "^2.0.3"
+ colorette "^2.0.14"
+ commander "^10.0.1"
+ cross-spawn "^7.0.3"
+ envinfo "^7.7.3"
+ fastest-levenshtein "^1.0.12"
+ import-local "^3.0.2"
+ interpret "^3.1.1"
+ rechoir "^0.8.0"
+ webpack-merge "^5.7.3"
+
+webpack-dev-middleware@^5.3.1:
+ version "5.3.4"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517"
+ integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==
+ dependencies:
+ colorette "^2.0.10"
+ memfs "^3.4.3"
+ mime-types "^2.1.31"
+ range-parser "^1.2.1"
+ schema-utils "^4.0.0"
+
+webpack-dev-server@4.15.0:
+ version "4.15.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz#87ba9006eca53c551607ea0d663f4ae88be7af21"
+ integrity sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==
+ dependencies:
+ "@types/bonjour" "^3.5.9"
+ "@types/connect-history-api-fallback" "^1.3.5"
+ "@types/express" "^4.17.13"
+ "@types/serve-index" "^1.9.1"
+ "@types/serve-static" "^1.13.10"
+ "@types/sockjs" "^0.3.33"
+ "@types/ws" "^8.5.1"
+ ansi-html-community "^0.0.8"
+ bonjour-service "^1.0.11"
+ chokidar "^3.5.3"
+ colorette "^2.0.10"
+ compression "^1.7.4"
+ connect-history-api-fallback "^2.0.0"
+ default-gateway "^6.0.3"
+ express "^4.17.3"
+ graceful-fs "^4.2.6"
+ html-entities "^2.3.2"
+ http-proxy-middleware "^2.0.3"
+ ipaddr.js "^2.0.1"
+ launch-editor "^2.6.0"
+ open "^8.0.9"
+ p-retry "^4.5.0"
+ rimraf "^3.0.2"
+ schema-utils "^4.0.0"
+ selfsigned "^2.1.1"
+ serve-index "^1.9.1"
+ sockjs "^0.3.24"
+ spdy "^4.0.2"
+ webpack-dev-middleware "^5.3.1"
+ ws "^8.13.0"
+
+webpack-merge@^4.1.5:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d"
+ integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==
+ dependencies:
+ lodash "^4.17.15"
+
+webpack-merge@^5.7.3:
+ version "5.10.0"
+ resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177"
+ integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==
+ dependencies:
+ clone-deep "^4.0.1"
+ flat "^5.0.2"
+ wildcard "^2.0.0"
+
+webpack-sources@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+ integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack@5.82.0:
+ version "5.82.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.82.0.tgz#3c0d074dec79401db026b4ba0fb23d6333f88e7d"
+ integrity sha512-iGNA2fHhnDcV1bONdUu554eZx+XeldsaeQ8T67H6KKHl2nUSwX8Zm7cmzOA46ox/X1ARxf7Bjv8wQ/HsB5fxBg==
+ dependencies:
+ "@types/eslint-scope" "^3.7.3"
+ "@types/estree" "^1.0.0"
+ "@webassemblyjs/ast" "^1.11.5"
+ "@webassemblyjs/wasm-edit" "^1.11.5"
+ "@webassemblyjs/wasm-parser" "^1.11.5"
+ acorn "^8.7.1"
+ acorn-import-assertions "^1.7.6"
+ browserslist "^4.14.5"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^5.13.0"
+ es-module-lexer "^1.2.1"
+ eslint-scope "5.1.1"
+ events "^3.2.0"
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.2.9"
+ json-parse-even-better-errors "^2.3.1"
+ loader-runner "^4.2.0"
+ mime-types "^2.1.27"
+ neo-async "^2.6.2"
+ schema-utils "^3.1.2"
+ tapable "^2.1.1"
+ terser-webpack-plugin "^5.3.7"
+ watchpack "^2.4.0"
+ webpack-sources "^3.2.3"
+
+websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
+ integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
+ dependencies:
+ http-parser-js ">=0.5.1"
+ safe-buffer ">=5.1.0"
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
+ integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
+
+which@^1.2.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wildcard@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"
+ integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
+
+workerpool@6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
+ integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
+
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+ws@^8.13.0:
+ version "8.16.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
+ integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
+
+ws@~8.11.0:
+ version "8.11.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
+ integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
+
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yargs-parser@20.2.4:
+ version "20.2.4"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
+ integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
+
+yargs-parser@^20.2.2:
+ version "20.2.9"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+ integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs-unparser@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb"
+ integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==
+ dependencies:
+ camelcase "^6.0.0"
+ decamelize "^4.0.0"
+ flat "^5.0.2"
+ is-plain-obj "^2.1.0"
+
+yargs@16.2.0, yargs@^16.1.1:
+ version "16.2.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+ integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+ dependencies:
+ cliui "^7.0.2"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.0"
+ y18n "^5.0.5"
+ yargs-parser "^20.2.2"
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/screenshots/1_today.png b/screenshots/1_today.png
new file mode 100644
index 0000000..eb9e47d
Binary files /dev/null and b/screenshots/1_today.png differ
diff --git a/screenshots/2_form.png b/screenshots/2_form.png
new file mode 100644
index 0000000..9de45f7
Binary files /dev/null and b/screenshots/2_form.png differ
diff --git a/screenshots/3_simple.png b/screenshots/3_simple.png
new file mode 100644
index 0000000..a9b849e
Binary files /dev/null and b/screenshots/3_simple.png differ
diff --git a/screenshots/4_light_place.png b/screenshots/4_light_place.png
new file mode 100644
index 0000000..a8ba652
Binary files /dev/null and b/screenshots/4_light_place.png differ
diff --git a/screenshots/5_dark_person.png b/screenshots/5_dark_person.png
new file mode 100644
index 0000000..828dc0f
Binary files /dev/null and b/screenshots/5_dark_person.png differ
diff --git a/screenshots/6_form_setup_zone.png b/screenshots/6_form_setup_zone.png
new file mode 100644
index 0000000..9d92750
Binary files /dev/null and b/screenshots/6_form_setup_zone.png differ
diff --git a/screenshots/7_form_setup_name.png b/screenshots/7_form_setup_name.png
new file mode 100644
index 0000000..7d6f337
Binary files /dev/null and b/screenshots/7_form_setup_name.png differ
diff --git a/screenshots/8_settings_location_format.png b/screenshots/8_settings_location_format.png
new file mode 100644
index 0000000..7030996
Binary files /dev/null and b/screenshots/8_settings_location_format.png differ
diff --git a/screenshots/9_settings_date_format.png b/screenshots/9_settings_date_format.png
new file mode 100644
index 0000000..1c4605b
Binary files /dev/null and b/screenshots/9_settings_date_format.png differ
diff --git a/styleGuide/darkMode.png b/styleGuide/darkMode.png
new file mode 100644
index 0000000..9676b49
Binary files /dev/null and b/styleGuide/darkMode.png differ
diff --git a/styleGuide/lightMode.png b/styleGuide/lightMode.png
new file mode 100644
index 0000000..2674601
Binary files /dev/null and b/styleGuide/lightMode.png differ