Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# DESIGN.md — Frogo Keyboard Visual Direction & Design System

> Anti-Slop Mode: DURING
> Dial: ENERGY 2 / RHYTHM 2 / MOTION 2
> Reading this as: Native Android Keyboard & Tooling App for everyday typing and productivity users, in a Tactile & Modern Tooling Material 3 style.

---

## 1. Brand Identity & Character

Frogo Keyboard is a precision mobile input method and utility suite. Rather than appearing as a generic AI template application or a whimsical toy, the design embodies **Tactile & Modern Tooling**:
- **Tone:** Professional, reliable, tactile, and clear.
- **Surface Feel:** Solid, grounded surfaces with clean 1dp borders instead of muddy floating blur or excessive drop shadows.
- **Keycap Feel:** High-contrast keycaps with distinct touch feedback, crisp typographic legends, and ergonomic spacing.
- **Copywriting:** Direct, human, informative text. Strict ban on em dashes (`—`) and generic AI marketing buzzwords.

---

## 2. Core Color Palette (WCAG AA Compliant)

Capped at 2 core colors + 1 accent color (R-29), with neutral ground bases:

### Light Theme
- **Primary (Mechanical Steel Blue):** `#2563EB` (Contrast ratio >= 4.5:1 on white/surface)
- **Primary Container:** `#EFF6FF` (Subtle tint for selected state)
- **Background:** `#F8FAFC` (Clean slate-white ground plane)
- **Surface:** `#FFFFFF` (Card surfaces)
- **Border / Outline:** `#E2E8F0` (Tactile 1dp border)
- **On Surface / Text Primary:** `#0F172A` (Slate 900, 14.2:1 contrast ratio)
- **On Surface Variant / Text Secondary:** `#475569` (Slate 600, 5.8:1 contrast ratio)

### Dark Theme
- **Primary (Precision Steel Blue Dark):** `#3B82F6` (Contrast ratio >= 4.5:1 on dark surfaces)
- **Primary Container:** `#1E3A8A` (Deep navy container)
- **Background:** `#0F172A` (Deep slate ground plane)
- **Surface:** `#1E293B` (Elevated slate surface)
- **Border / Outline:** `#334155` (Crisp dark tactile border)
- **On Surface / Text Primary:** `#F8FAFC` (Crisp white-slate)
- **On Surface Variant / Text Secondary:** `#94A3B8` (Legible muted slate)

### Semantic Status Colors
- **Success (Active / Enabled):** `#15803D` (Light) / `#22C55E` (Dark)
- **Warning (Attention / Action Needed):** `#B45309` (Light) / `#F59E0B` (Dark)
- **Error (Failed / Inactive):** `#B91C1C` (Light) / `#EF4444` (Dark)

---

## 3. Shape, Radii, & Elevation Hierarchy

No uniform pill shapes everywhere (R-11). Distinct functional radii:
- **Card Surfaces:** `12.dp` (Clean, grounded corners)
- **Chips, Keycaps, & Buttons:** `8.dp` (Ergonomic tap target)
- **Dialogs & Bottom Sheets:** `16.dp` (Modal focus)
- **Status Indicators / Avatars:** `CircleShape` (Only for circular glyph badges)
- **Elevation:** Flat `0.dp` or subtle `1.dp` accompanied by a `1.dp` crisp border (`BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)`). No generic floating clouds (R-12).

---

## 4. Typography Scale

- **Title Large:** 22sp, Bold, 28sp line height (Screen headers)
- **Title Medium:** 18sp, SemiBold, 24sp line height (Section titles)
- **Title Small:** 15sp, SemiBold, 20sp line height (Card titles)
- **Body Large:** 16sp, Normal, 24sp line height (Primary descriptions)
- **Body Medium:** 14sp, Normal, 20sp line height (Secondary descriptions, form labels)
- **Body Small:** 12sp, Normal, 16sp line height (Metadata, hints)
- **Label Large:** 14sp, Medium, 20sp line height (Button text, action labels)
- **Label Small:** 11sp, SemiBold, 14sp line height (Badges, indicators)

---

## 5. Anti-Slop Enforcement Rules

1. **R-01 (Color):** Zero generic purple-to-pink gradients or neon glow. All colors follow the palette above.
2. **R-02 (Copywriting):** Absolutely NO em dash (`—`) in any UI text or code comments. Use comma, period, colon, or parentheses.
3. **R-03 (Mobile Responsiveness):** Minimum 48dp tap target for interactive elements. Zero horizontal scroll overflow on standard screens.
4. **R-06 (Typography):** No uppercase tracking slop (e.g. replace `NO DATA FOUND` with clear descriptive copy).
5. **R-10 & R-13 (Glass & Glow):** No blur overlays or decorative glows. Clean, matte, tactile surfaces.
6. **R-16 (Buzzwords):** No words like "AI Powered", "Seamless", "Revolutionary", "Next-Gen". Use direct descriptive language.
7. **R-25 (Contrast):** Every text and icon pair satisfies WCAG AA (>= 4.5:1 for normal text).
8. **R-27 (UI States):** Empty, Loading, and Content states present and properly styled on every screen.
9. **R-34 (Theme Integrity):** Light and Dark themes both fully functional and tested.
19 changes: 5 additions & 14 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -115,19 +115,7 @@ kotlin {
dependencies {

val composeBom = platform(libs.androidx.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)

implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)

implementation(project(DependencyGradle.MODULE_LIB_FROGO_KEYBOARD))
implementation(project(DependencyGradle.MODULE_LIB_KEYBOARD))

implementation(libs.androidx.work.ktx)
implementation(libs.google.hilt)
Expand All @@ -136,10 +124,13 @@ dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)

ksp(libs.github.glide.compiler)
ksp(libs.google.hilt.compiler)
ksp(libs.androidx.room.compiler)
ksp(libs.androidx.lifecycle.compiler)

androidTestImplementation(composeBom)
testImplementation(libs.junit)
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
debugImplementation(libs.androidx.compose.ui.tooling)

}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name=".MainApp"
android:allowBackup="true"
android:networkSecurityConfig="@xml/network_security_config"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/frogobox/appkeyboard/common/core/Resource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.frogobox.appkeyboard.common.core

import androidx.annotation.Keep

/**
* Standard sealed Resource wrapper for asynchronous operations and network requests.
*/
@Keep
sealed class Resource<out T> {
data class Success<out T>(val data: T) : Resource<T>()
data class Error(val message: String, val cause: Throwable? = null, val code: Int? = null) : Resource<Nothing>()
data object Loading : Resource<Nothing>()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.frogobox.appkeyboard.data.remote

import com.frogobox.appkeyboard.data.remote.model.DataApiResponse
import retrofit2.Response
import retrofit2.http.GET

/**
* Retrofit network service interface for fetching data from remote endpoint.
*/
interface DataApiService {

@GET("api/data.json")
suspend fun getData(): Response<DataApiResponse>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.frogobox.appkeyboard.data.remote.model

import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName

/**
* Root data model for remote API response from /api/data.json.
* Annotated with @Keep and @SerializedName for safe serialization/deserialization.
*/
@Keep
data class DataApiResponse(
@SerializedName("code")
val code: Int? = null,

@SerializedName("status")
val status: String? = null,

@SerializedName("message")
val message: String? = null,

@SerializedName("total")
val total: Int? = null,

@SerializedName("lastUpdated")
val lastUpdated: String? = null,

@SerializedName("data")
val data: List<DataItemResponse>? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.frogobox.appkeyboard.data.remote.model

import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName

/**
* Data model for individual items returned from remote API.
* Annotated with @Keep and @SerializedName to ensure ProGuard obfuscation safety.
*/
@Keep
data class DataItemResponse(
@SerializedName("id")
val id: Int? = null,

@SerializedName("title")
val title: String? = null,

@SerializedName("body")
val body: String? = null,

@SerializedName("description")
val description: String? = null,

@SerializedName("category")
val category: String? = null,

@SerializedName("createdAt")
val createdAt: String? = null,

@SerializedName("isActive")
val isActive: Boolean? = null,

@SerializedName("rowIndex")
val rowIndex: Int? = null,

@SerializedName("isVideo")
val isVideo: Boolean? = null,

@SerializedName("fileSize")
val fileSize: String? = null,

@SerializedName("caption")
val caption: String? = null,

@SerializedName("uploaderName")
val uploaderName: String? = null,

@SerializedName("uploadTimestamp")
val uploadTimestamp: String? = null,

@SerializedName("driveLink")
val driveLink: String? = null,

@SerializedName("thumbnailUrl")
val thumbnailUrl: String? = null
) {
val displayIndex: Int
get() = rowIndex ?: id ?: 0

val displayTitle: String
get() = caption?.takeIf { it.isNotBlank() }
?: title?.takeIf { it.isNotBlank() }
?: uploaderName?.takeIf { it.isNotBlank() }
?: "Item #$displayIndex"

val displaySubtitle: String?
get() = uploaderName?.takeIf { it.isNotBlank() }
?: category?.takeIf { it.isNotBlank() }

val displayBody: String?
get() = body?.takeIf { it.isNotBlank() }
?: description?.takeIf { it.isNotBlank() }
?: caption?.takeIf { it.isNotBlank() }

val displayTimestamp: String?
get() = uploadTimestamp?.takeIf { it.isNotBlank() }
?: createdAt?.takeIf { it.isNotBlank() }
}
26 changes: 17 additions & 9 deletions app/src/main/java/com/frogobox/appkeyboard/di/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
package com.frogobox.appkeyboard.di

import android.content.Context
import com.frogobox.appkeyboard.data.remote.ApiService
import com.frogobox.appkeyboard.data.remote.DataApiService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

/**
* Created by Faisal Amir on 06/01/23
* -----------------------------------------
* E-mail : faisalamircs@gmail.com
* Github : github.com/amirisback
* -----------------------------------------
* Copyright (C) Frogobox ID / amirisback
* All rights reserved
* Hilt module providing network dependencies and services.
*/


@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {

companion object {
const val BASE_URL = "http://192.168.100.6:3000/"
}

@Provides
@Singleton
fun provideDataApiService(@ApplicationContext context: Context): DataApiService {
return ApiService.create<DataApiService>(context, BASE_URL)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,17 @@ package com.frogobox.appkeyboard.di

import com.frogobox.appkeyboard.repository.autotext.AutoTextRepository
import com.frogobox.appkeyboard.repository.autotext.AutoTextRepositoryImpl
import com.frogobox.appkeyboard.repository.data.DataApiRepository
import com.frogobox.appkeyboard.repository.data.DataApiRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

/**
* Created by Faisal Amir on 06/01/23
* -----------------------------------------
* E-mail : faisalamircs@gmail.com
* Github : github.com/amirisback
* -----------------------------------------
* Copyright (C) Frogobox ID / amirisback
* All rights reserved
* Hilt module binding repository implementations to their interfaces.
*/

@Module(includes = [
NetworkModule::class,
ServiceModule::class,
Expand All @@ -28,4 +24,8 @@ abstract class RepositoryModule {
@Binds
abstract fun getAutoTextRepository(repository: AutoTextRepositoryImpl): AutoTextRepository

@Binds
@Singleton
abstract fun bindDataApiRepository(repository: DataApiRepositoryImpl): DataApiRepository

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.frogobox.appkeyboard.repository.data

import com.frogobox.appkeyboard.common.core.Resource
import com.frogobox.appkeyboard.data.remote.model.DataApiResponse
import kotlinx.coroutines.flow.Flow

/**
* Repository interface defining operations for fetching remote API data.
*/
interface DataApiRepository {

fun fetchDataStream(): Flow<Resource<DataApiResponse>>

suspend fun fetchData(): Resource<DataApiResponse>

}
Loading
Loading