Compare commits
2 commits
1362daee53
...
875a319ddd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
875a319ddd | ||
|
|
68721d8ddf |
14 changed files with 194 additions and 150 deletions
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -47,6 +47,7 @@ settings/ SettingsScreen, SettingsViewModel, SettingsComponents, SecurityS
|
|||
ui/
|
||||
theme/ Color (P3), Theme (multi-theme), Type, Oklab, Dither
|
||||
theme/themes/ AccentScheme, AmberTheme, BlauTheme, MonoTheme, AuroraTheme, ThemeRegistry
|
||||
icon/ KaizenIcons (central registry, Lucide base, custom override layer)
|
||||
shape/ SquircleShape (superellipse), KaizenShapes (token system)
|
||||
effect/ GlassSurface (frosted glass), Shadow (multi-layer stack)
|
||||
motion/ Motion (easing curves, spring specs, duration tokens)
|
||||
|
|
@ -65,7 +66,7 @@ ui/
|
|||
|
||||
Full spec: `DESIGN.md`.
|
||||
|
||||
**Six pillars** (load-bearing — if any regress, the design regresses):
|
||||
**Seven pillars** (load-bearing — if any regress, the design regresses):
|
||||
|
||||
1. **P3 color pipeline** — all tokens in DisplayP3 color space (`Color.kt`). `window.colorMode = COLOR_MODE_HDR` (API 34+) / `COLOR_MODE_WIDE_COLOR_GAMUT` (API 26+) in `MainActivity`. No `Color(0xFF…)` for brand colors outside `Color.kt`.
|
||||
2. **Squircle shapes** — iOS-style superellipse with G2 continuity (`ui/shape/Squircle.kt`). Token system `KaizenShapes.{xs,sm,md,lg,xl,pill,circle}`. Replaces all `RoundedCornerShape`.
|
||||
|
|
@ -73,14 +74,17 @@ Full spec: `DESIGN.md`.
|
|||
4. **Multi-layer shadows** — `ShadowStack` with 2–4 overlapping layers (`KaizenShadows.level0..4`). Dark mode multiplies alphas ×1.8. Replaces all `Modifier.shadow()`.
|
||||
5. **4-theme system** — Amber/Blau/Mono/Aurora. Each theme = `KaizenAccentScheme` (primary + 4 blob colors). Neutrals are theme-independent. `LocalKaizenAccent` CompositionLocal. Theme selection wired end-to-end: `SettingsViewModel.selectedTheme` → `MainActivity` → `KaizenTheme(themeId=...)`. No server sync (web stores in localStorage, app stores locally). Appearance (Light/Dark/System) also wired end-to-end.
|
||||
6. **Sensor-reactive motion** — `rememberTiltState()` provides smoothed tilt `Offset(-1..1)`. Uses `TYPE_GAME_ROTATION_VECTOR` (fused, no drift). Auto-disables on battery saver, reduced motion, background, extreme tilt.
|
||||
7. **KaizenIcons** (`ui/icon/KaizenIcons.kt`) — centralized icon registry, same architecture as Google's Luminous Symbols. Lucide (1.5px stroke, same library as web frontend) as base, with a custom override layer for brand-critical icons. Every UI component references `KaizenIcons.Send`, `KaizenIcons.Lock`, etc. — never a library directly. Zero Material Icon imports remain. To add a custom icon: create an `ImageVector` literal (hand-drawn SVG converted to Compose path data) and swap the getter in `KaizenIcons.kt` — all usages update instantly, no call-site changes. Icons grouped semantically: Navigation, Chat, Modes, Conversation, Files, Search, Settings, Scroll. `com.composables:icons-lucide-android:1.0.0`.
|
||||
|
||||
**Motion tokens** (`ui/motion/Motion.kt`): `EaseSpring` (overshoot), `EaseSmooth` (expo-out), `EaseEmphasized` (M3), `SpringSnappy/Default/Gentle`. All durations centralised in `Durations` object.
|
||||
|
||||
**Migration status:** All UI components migrated to the design system. Sidebar, ChatInput, ChatScreen, LoginScreen, ModelSheet, SettingsCard, SettingsScreen use `GlassSurface` + `KaizenShapes` + `kaizenShadow`. Only Markdown code-block shapes remain as `RoundedCornerShape` (intentionally rectangular for code).
|
||||
**Migration status:** All UI components migrated to the design system. Sidebar, ChatInput, ChatScreen, LoginScreen, ModelSheet, SettingsCard, SettingsScreen use `GlassSurface` + `KaizenShapes` + `kaizenShadow` + `KaizenIcons`. Only Markdown code-block shapes remain as `RoundedCornerShape` (intentionally rectangular for code). All icons migrated from Material Icons Rounded to KaizenIcons/Lucide — zero `Icons.Rounded.*` imports remain.
|
||||
|
||||
**Theme switching fully wired:** All 7 files that previously imported hardcoded `Amber`/`AmberDeep`/`OnAmber`/`BlobX` now read from `LocalKaizenAccent.current`. Changing theme in Settings immediately updates: message bubbles, send button, orb, mesh background blobs, sidebar highlights, model sheet, stream blocks, login screen, settings toggles. No hardcoded amber imports remain outside `Color.kt` and theme definitions.
|
||||
|
||||
**Floating bar scrims:** Top bar (hamburger + model pill) and bottom dock (mode pills + input) have vertical gradient scrims (`MaterialTheme.colorScheme.background` → transparent) to prevent message content from bleeding through. Model pill opacity increased to 0.92.
|
||||
**Floating bar scrims:** Top bar and bottom dock have vertical gradient scrims (`MaterialTheme.colorScheme.background` → transparent) to prevent message content from bleeding through.
|
||||
|
||||
**Unified top bar:** Sidebar trigger (menu icon) and model name merged into a single `GlassSurface` pill (38dp, `KaizenShapes.pill`). Menu icon (17dp, muted) → 3dp dot divider → model name (13sp Medium, 180dp max with ellipsis). No chevron — pill is self-evidently tappable. Matches the TokenBadge glass language on the right.
|
||||
|
||||
**KaizenOrb** (`chat/Background.kt`) uses `rememberTiltState()` from `ui/sensor/` instead of inline sensor code. Breath animation uses `Durations.ORB_BREATH` / `ORB_BREATH_STREAMING` from `ui/motion/`.
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ dependencies {
|
|||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.compose.icons.lucide)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
|
|
|
|||
|
|
@ -39,10 +39,6 @@ import androidx.compose.foundation.layout.widthIn
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.CameraAlt
|
||||
import androidx.compose.material.icons.rounded.Call
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -79,28 +75,12 @@ import java.time.format.DateTimeFormatter
|
|||
import java.util.Locale
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.rounded.AudioFile
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Code
|
||||
import androidx.compose.material.icons.rounded.Image
|
||||
import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile
|
||||
import androidx.compose.material.icons.rounded.PictureAsPdf
|
||||
import androidx.compose.material.icons.rounded.VideoFile
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.material.icons.rounded.Bolt
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.ContentCopy
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Mic
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import dev.kaizen.app.haptics.rememberHaptics
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material.icons.rounded.Psychology
|
||||
import androidx.compose.material.icons.rounded.Tune
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
|
|
@ -111,6 +91,7 @@ import kotlinx.coroutines.withContext
|
|||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.TimeUnit
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
@Composable
|
||||
fun KaizenHeader(modifier: Modifier = Modifier) {
|
||||
|
|
@ -199,11 +180,11 @@ fun ModePillsRow(
|
|||
.padding(horizontal = 13.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(Icons.Rounded.Psychology, null, tint = cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||
Icon(KaizenIcons.Brain, null, tint = cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(reasoningPreset.label), color = cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(Icons.Rounded.ExpandMore, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp))
|
||||
Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp))
|
||||
}
|
||||
DropdownMenu(expanded = showReasoningMenu, onDismissRequest = { showReasoningMenu = false }) {
|
||||
ReasoningPreset.entries.forEach { preset ->
|
||||
|
|
@ -234,11 +215,11 @@ fun ModePillsRow(
|
|||
.padding(horizontal = 13.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(Icons.Rounded.Tune, null, tint = if (anyEnabled) accent.primary else cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||
Icon(KaizenIcons.SlidersHorizontal, null, tint = if (anyEnabled) accent.primary else cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.mode_sampling), color = if (anyEnabled) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(Icons.Rounded.ExpandMore, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp))
|
||||
Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp))
|
||||
}
|
||||
DropdownMenu(expanded = showSamplingPopup, onDismissRequest = { showSamplingPopup = false }) {
|
||||
Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp).width(240.dp)) {
|
||||
|
|
@ -332,7 +313,7 @@ fun TokenBadge(used: Int, contextLength: Int, modifier: Modifier = Modifier) {
|
|||
Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(Icons.Rounded.Bolt, null, tint = accent.primary, modifier = Modifier.size(14.dp))
|
||||
Icon(KaizenIcons.Zap, null, tint = accent.primary, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
"${formatK(used)} / ${formatK(contextLength)}",
|
||||
|
|
@ -416,7 +397,7 @@ fun MessageRow(
|
|||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
MessageActionButton(
|
||||
icon = if (copied) Icons.Rounded.Check else Icons.Rounded.ContentCopy,
|
||||
icon = if (copied) KaizenIcons.Check else KaizenIcons.Copy,
|
||||
tint = if (copied) Color(0xFF10B981) else cs.onSurfaceVariant.copy(alpha = 0.35f),
|
||||
) {
|
||||
haptics.tick()
|
||||
|
|
@ -428,7 +409,7 @@ fun MessageRow(
|
|||
}
|
||||
}
|
||||
onDelete?.let { del ->
|
||||
MessageActionButton(Icons.Rounded.Delete, Color(0xFFEF4444).copy(alpha = 0.6f)) {
|
||||
MessageActionButton(KaizenIcons.Trash, Color(0xFFEF4444).copy(alpha = 0.6f)) {
|
||||
haptics.tick(); del(message.wireId)
|
||||
}
|
||||
}
|
||||
|
|
@ -469,7 +450,7 @@ fun MessageRow(
|
|||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
MessageActionButton(
|
||||
icon = if (copied) Icons.Rounded.Check else Icons.Rounded.ContentCopy,
|
||||
icon = if (copied) KaizenIcons.Check else KaizenIcons.Copy,
|
||||
tint = if (copied) Color(0xFF10B981) else cs.onSurfaceVariant.copy(alpha = 0.35f),
|
||||
) {
|
||||
haptics.tick()
|
||||
|
|
@ -481,12 +462,12 @@ fun MessageRow(
|
|||
}
|
||||
}
|
||||
onRegenerate?.let { regen ->
|
||||
MessageActionButton(Icons.Rounded.Refresh, cs.onSurfaceVariant.copy(alpha = 0.35f)) {
|
||||
MessageActionButton(KaizenIcons.RefreshCw, cs.onSurfaceVariant.copy(alpha = 0.35f)) {
|
||||
haptics.tick(); regen(message.wireId)
|
||||
}
|
||||
}
|
||||
onDelete?.let { del ->
|
||||
MessageActionButton(Icons.Rounded.Delete, Color(0xFFEF4444).copy(alpha = 0.6f)) {
|
||||
MessageActionButton(KaizenIcons.Trash, Color(0xFFEF4444).copy(alpha = 0.6f)) {
|
||||
haptics.tick(); del(message.wireId)
|
||||
}
|
||||
}
|
||||
|
|
@ -575,7 +556,7 @@ fun ChatInput(
|
|||
}
|
||||
Row(Modifier.padding(start = 6.dp, end = 8.dp, top = 10.dp, bottom = 10.dp), verticalAlignment = Alignment.Bottom) {
|
||||
Box(Modifier.size(42.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick), contentAlignment = Alignment.Center) {
|
||||
Icon(Icons.Rounded.Add, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
Box(Modifier.weight(1f).heightIn(min = 42.dp).padding(horizontal = 6.dp), contentAlignment = Alignment.CenterStart) {
|
||||
if (value.isEmpty()) Text(stringResource(R.string.chat_input_placeholder), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 15.sp)
|
||||
|
|
@ -591,7 +572,7 @@ fun ChatInput(
|
|||
SendButton(enabled = canSend, onClick = onSend)
|
||||
} else {
|
||||
Box(Modifier.size(44.dp).clip(KaizenShapes.circle), contentAlignment = Alignment.Center) {
|
||||
Icon(Icons.Rounded.Mic, stringResource(R.string.chat_call), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
Icon(KaizenIcons.Mic, stringResource(R.string.chat_call), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -611,11 +592,11 @@ private fun PendingFileChip(pf: PendingFile, onRemove: () -> Unit) {
|
|||
NetworkImage(url = pf.uploaded.url, contentDescription = pf.name, modifier = Modifier.fillMaxSize().clip(KaizenShapes.xs))
|
||||
} else {
|
||||
val icon = when {
|
||||
pf.mimeType.startsWith("image/") -> Icons.Rounded.Image
|
||||
pf.mimeType.startsWith("video/") -> Icons.Rounded.VideoFile
|
||||
pf.mimeType.startsWith("audio/") -> Icons.Rounded.AudioFile
|
||||
pf.mimeType == "application/pdf" -> Icons.Rounded.PictureAsPdf
|
||||
else -> Icons.AutoMirrored.Rounded.InsertDriveFile
|
||||
pf.mimeType.startsWith("image/") -> KaizenIcons.Image
|
||||
pf.mimeType.startsWith("video/") -> KaizenIcons.FileVideo
|
||||
pf.mimeType.startsWith("audio/") -> KaizenIcons.FileAudio
|
||||
pf.mimeType == "application/pdf" -> KaizenIcons.FileText
|
||||
else -> KaizenIcons.File
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(icon, null, tint = cs.onSurfaceVariant, modifier = Modifier.size(20.dp))
|
||||
|
|
@ -629,14 +610,14 @@ private fun PendingFileChip(pf: PendingFile, onRemove: () -> Unit) {
|
|||
}
|
||||
if (pf.error != null) {
|
||||
Box(Modifier.fillMaxSize().background(Color(0xFFDC2626).copy(alpha = 0.3f)), contentAlignment = Alignment.Center) {
|
||||
Icon(Icons.Rounded.Close, stringResource(R.string.chat_error), tint = Color.White, modifier = Modifier.size(18.dp))
|
||||
Icon(KaizenIcons.X, stringResource(R.string.chat_error), tint = Color.White, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier.align(Alignment.TopEnd).padding(2.dp).size(18.dp).clip(KaizenShapes.circle).background(Color.Black.copy(alpha = 0.5f)).clickable(onClick = onRemove),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(Icons.Rounded.Close, stringResource(R.string.chat_remove), tint = Color.White, modifier = Modifier.size(12.dp))
|
||||
Icon(KaizenIcons.X, stringResource(R.string.chat_remove), tint = Color.White, modifier = Modifier.size(12.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -718,11 +699,11 @@ private fun FileChip(attachment: Attachment) {
|
|||
val isDark = isSystemInDarkTheme()
|
||||
val bg = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f)
|
||||
val icon = when (attachment.kind) {
|
||||
"pdf" -> Icons.Rounded.PictureAsPdf
|
||||
"audio" -> Icons.Rounded.AudioFile
|
||||
"video" -> Icons.Rounded.VideoFile
|
||||
"code" -> Icons.Rounded.Code
|
||||
else -> Icons.AutoMirrored.Rounded.InsertDriveFile
|
||||
"pdf" -> KaizenIcons.FileText
|
||||
"audio" -> KaizenIcons.FileAudio
|
||||
"video" -> KaizenIcons.FileVideo
|
||||
"code" -> KaizenIcons.Code
|
||||
else -> KaizenIcons.File
|
||||
}
|
||||
Row(Modifier.clip(KaizenShapes.xs).background(bg).padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, null, tint = cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
|
||||
|
|
@ -746,9 +727,9 @@ fun AttachmentMenu(
|
|||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AttachPill(Icons.Rounded.CameraAlt, stringResource(R.string.attach_camera), accent.primary, isDark, onCamera)
|
||||
AttachPill(Icons.Rounded.Image, stringResource(R.string.attach_gallery), Color(0xFF6366F1), isDark, onGallery)
|
||||
AttachPill(Icons.AutoMirrored.Rounded.InsertDriveFile, stringResource(R.string.attach_file), Color(0xFF10B981), isDark, onFile)
|
||||
AttachPill(KaizenIcons.Camera, stringResource(R.string.attach_camera), accent.primary, isDark, onCamera)
|
||||
AttachPill(KaizenIcons.Image, stringResource(R.string.attach_gallery), Color(0xFF6366F1), isDark, onGallery)
|
||||
AttachPill(KaizenIcons.File, stringResource(R.string.attach_file), Color(0xFF10B981), isDark, onFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
package dev.kaizen.app.chat
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Adjust
|
||||
import androidx.compose.material.icons.rounded.AutoAwesome
|
||||
import androidx.compose.material.icons.rounded.Image
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material.icons.rounded.Movie
|
||||
import androidx.compose.material.icons.rounded.MusicNote
|
||||
import androidx.compose.material.icons.rounded.Tune
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -15,6 +7,7 @@ import dev.kaizen.app.R
|
|||
import dev.kaizen.app.net.Attachment
|
||||
import dev.kaizen.app.net.SearchSource
|
||||
import dev.kaizen.app.net.ToolEvent
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
enum class Role { User, Assistant }
|
||||
|
||||
|
|
@ -54,21 +47,21 @@ data class Message(
|
|||
data class Suggestion(val labelRes: Int, val promptRes: Int, val icon: ImageVector)
|
||||
|
||||
val suggestions = listOf(
|
||||
Suggestion(R.string.suggest_brainstorm, R.string.suggest_brainstorm_prompt, Icons.Rounded.AutoAwesome),
|
||||
Suggestion(R.string.suggest_image, R.string.suggest_image_prompt, Icons.Rounded.Image),
|
||||
Suggestion(R.string.suggest_video, R.string.suggest_video_prompt, Icons.Rounded.Movie),
|
||||
Suggestion(R.string.suggest_web, R.string.suggest_web_prompt, Icons.Rounded.Language),
|
||||
Suggestion(R.string.suggest_brainstorm, R.string.suggest_brainstorm_prompt, KaizenIcons.Sparkles),
|
||||
Suggestion(R.string.suggest_image, R.string.suggest_image_prompt, KaizenIcons.Image),
|
||||
Suggestion(R.string.suggest_video, R.string.suggest_video_prompt, KaizenIcons.Video),
|
||||
Suggestion(R.string.suggest_web, R.string.suggest_web_prompt, KaizenIcons.Globe),
|
||||
)
|
||||
|
||||
data class ChatMode(val labelRes: Int, val icon: ImageVector)
|
||||
|
||||
val chatModes = listOf(
|
||||
ChatMode(R.string.mode_standard, Icons.Rounded.Adjust),
|
||||
ChatMode(R.string.mode_sampling, Icons.Rounded.Tune),
|
||||
ChatMode(R.string.mode_image, Icons.Rounded.Image),
|
||||
ChatMode(R.string.mode_search, Icons.Rounded.Language),
|
||||
ChatMode(R.string.mode_video, Icons.Rounded.Movie),
|
||||
ChatMode(R.string.mode_audio, Icons.Rounded.MusicNote),
|
||||
ChatMode(R.string.mode_standard, KaizenIcons.CircleDot),
|
||||
ChatMode(R.string.mode_sampling, KaizenIcons.SlidersHorizontal),
|
||||
ChatMode(R.string.mode_image, KaizenIcons.Image),
|
||||
ChatMode(R.string.mode_search, KaizenIcons.Globe),
|
||||
ChatMode(R.string.mode_video, KaizenIcons.Video),
|
||||
ChatMode(R.string.mode_audio, KaizenIcons.Music),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@ import androidx.compose.foundation.layout.widthIn
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -665,7 +662,7 @@ fun ChatScreen(
|
|||
.clickable { scope.launch { listState.animateScrollToItem(0) } },
|
||||
) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(Icons.Rounded.KeyboardArrowUp, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp))
|
||||
Icon(KaizenIcons.ArrowUp, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
} else if (!atBottom && canScrollDown) {
|
||||
|
|
@ -681,7 +678,7 @@ fun ChatScreen(
|
|||
.clickable { scope.launch { listState.animateScrollToItem(messages.size - 1) } },
|
||||
) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(Icons.Rounded.KeyboardArrowDown, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp))
|
||||
Icon(KaizenIcons.ArrowDown, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -730,7 +727,7 @@ fun ChatScreen(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Menu,
|
||||
imageVector = KaizenIcons.Menu,
|
||||
contentDescription = context.getString(R.string.chat_open_menu),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(17.dp),
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.ContentCopy
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -49,6 +46,7 @@ import androidx.compose.ui.unit.sp
|
|||
import dev.kaizen.app.haptics.rememberHaptics
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
private sealed interface MdBlock {
|
||||
data class Paragraph(val text: String) : MdBlock
|
||||
|
|
@ -526,7 +524,7 @@ private fun CopyButton(code: String) {
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (copied) Icons.Rounded.Check else Icons.Rounded.ContentCopy,
|
||||
imageVector = if (copied) KaizenIcons.Check else KaizenIcons.Copy,
|
||||
contentDescription = "Kopieren",
|
||||
tint = if (copied) Color(0xFF22C55E) else cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(14.dp),
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.items
|
||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -25,6 +23,7 @@ import dev.kaizen.app.R
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||
import dev.kaizen.app.haptics.rememberHaptics
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
/** Human label + composite-id resolver for the top pill — falls back to a de-prefixed id. */
|
||||
fun modelDisplayName(modelId: String, models: List<KaizenModel>): String =
|
||||
|
|
@ -52,7 +51,7 @@ fun ModelPill(label: String, onClick: () -> Unit, modifier: Modifier = Modifier)
|
|||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
Icons.Rounded.ExpandMore,
|
||||
KaizenIcons.ChevronDown,
|
||||
contentDescription = null,
|
||||
tint = cs.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp),
|
||||
|
|
@ -139,7 +138,7 @@ fun ModelSheet(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.Search,
|
||||
KaizenIcons.Search,
|
||||
contentDescription = null,
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
modifier = Modifier.size(18.dp)
|
||||
|
|
@ -167,7 +166,7 @@ fun ModelSheet(
|
|||
}
|
||||
if (query.isNotEmpty()) {
|
||||
Icon(
|
||||
Icons.Rounded.Close,
|
||||
KaizenIcons.X,
|
||||
contentDescription = stringResource(R.string.model_search_clear),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
|
|
@ -294,7 +293,7 @@ private fun ModelRow(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (favorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
|
||||
imageVector = if (favorite) KaizenIcons.Star else KaizenIcons.StarOff,
|
||||
contentDescription = stringResource(R.string.model_favorite),
|
||||
tint = if (favorite) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.35f),
|
||||
modifier = Modifier.size(20.dp),
|
||||
|
|
@ -304,7 +303,7 @@ private fun ModelRow(
|
|||
if (selected) {
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Icon(
|
||||
Icons.Rounded.Check,
|
||||
KaizenIcons.Check,
|
||||
contentDescription = stringResource(R.string.model_selected),
|
||||
tint = accent.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.Logout
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Edit
|
||||
import androidx.compose.material.icons.rounded.Lock
|
||||
import androidx.compose.material.icons.rounded.LockOpen
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.material.icons.rounded.PushPin
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.material3.Icon
|
||||
|
|
@ -70,6 +59,7 @@ import java.time.LocalDate
|
|||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
private data class DateLabels(val today: String, val yesterday: String, val earlier: String, val pinned: String)
|
||||
|
||||
|
|
@ -156,7 +146,7 @@ fun KaizenSidebar(
|
|||
.clickable { onNewChat() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Rounded.Edit, stringResource(R.string.chat_new), tint = cs.onBackground, modifier = Modifier.size(17.dp))
|
||||
Icon(KaizenIcons.Pencil, stringResource(R.string.chat_new), tint = cs.onBackground, modifier = Modifier.size(17.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +161,7 @@ fun KaizenSidebar(
|
|||
.padding(horizontal = 14.dp, vertical = 9.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Rounded.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.45f), modifier = Modifier.size(16.dp))
|
||||
Icon(KaizenIcons.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.45f), modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.chat_search), color = cs.onSurfaceVariant.copy(alpha = 0.45f), fontSize = 13.sp)
|
||||
}
|
||||
|
|
@ -261,7 +251,7 @@ fun KaizenSidebar(
|
|||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
Icons.Rounded.Settings,
|
||||
KaizenIcons.Settings,
|
||||
stringResource(R.string.sidebar_settings),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(18.dp),
|
||||
|
|
@ -275,7 +265,7 @@ fun KaizenSidebar(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Rounded.Logout,
|
||||
KaizenIcons.LogOut,
|
||||
stringResource(R.string.sidebar_logout),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(16.dp),
|
||||
|
|
@ -389,7 +379,7 @@ private fun HistoryItemRow(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (item.pinned) {
|
||||
Icon(Icons.Rounded.Star, null, tint = accent.primary.copy(alpha = 0.7f), modifier = Modifier.size(13.dp))
|
||||
Icon(KaizenIcons.Star, null, tint = accent.primary.copy(alpha = 0.7f), modifier = Modifier.size(13.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
}
|
||||
Text(
|
||||
|
|
@ -403,7 +393,7 @@ private fun HistoryItemRow(
|
|||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (item.locked) {
|
||||
Icon(Icons.Rounded.Lock, null, tint = accent.primary.copy(alpha = 0.5f), modifier = Modifier.size(13.dp))
|
||||
Icon(KaizenIcons.Lock, null, tint = accent.primary.copy(alpha = 0.5f), modifier = Modifier.size(13.dp))
|
||||
Spacer(Modifier.width(2.dp))
|
||||
}
|
||||
Box(
|
||||
|
|
@ -414,7 +404,7 @@ private fun HistoryItemRow(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.MoreVert,
|
||||
KaizenIcons.MoreVertical,
|
||||
stringResource(R.string.sidebar_more),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.35f),
|
||||
modifier = Modifier.size(16.dp),
|
||||
|
|
@ -430,15 +420,15 @@ private fun HistoryItemRow(
|
|||
shape = KaizenShapes.md,
|
||||
) {
|
||||
if (!item.locked) {
|
||||
MenuRow(Icons.Rounded.Edit, stringResource(R.string.sidebar_rename), cs.onBackground) { showMenu = false; onRename() }
|
||||
MenuRow(KaizenIcons.Pencil, stringResource(R.string.sidebar_rename), cs.onBackground) { showMenu = false; onRename() }
|
||||
}
|
||||
MenuRow(Icons.Rounded.PushPin, stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin), cs.onBackground) { showMenu = false; onTogglePin() }
|
||||
MenuRow(KaizenIcons.Pin, stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin), cs.onBackground) { showMenu = false; onTogglePin() }
|
||||
MenuRow(
|
||||
if (item.locked) Icons.Rounded.LockOpen else Icons.Rounded.Lock,
|
||||
if (item.locked) KaizenIcons.LockOpen else KaizenIcons.Lock,
|
||||
stringResource(if (item.locked) R.string.sidebar_unlock else R.string.sidebar_lock),
|
||||
cs.onBackground,
|
||||
) { showMenu = false; onToggleLock() }
|
||||
MenuRow(Icons.Rounded.Delete, stringResource(R.string.sidebar_delete), Color(0xFFEF4444)) { showMenu = false; onDelete() }
|
||||
MenuRow(KaizenIcons.Trash, stringResource(R.string.sidebar_delete), Color(0xFFEF4444)) { showMenu = false; onDelete() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,6 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.Error
|
||||
import androidx.compose.material.icons.rounded.ExpandLess
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material.icons.rounded.Psychology
|
||||
import androidx.compose.material.icons.rounded.AutoAwesome
|
||||
import androidx.compose.material.icons.rounded.Build
|
||||
import androidx.compose.material.icons.rounded.Description
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -50,6 +40,7 @@ import dev.kaizen.app.ui.shape.KaizenShapes
|
|||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
@Composable
|
||||
fun ReasoningBlock(reasoning: String, streaming: Boolean = false) {
|
||||
|
|
@ -75,7 +66,7 @@ fun ReasoningBlock(reasoning: String, streaming: Boolean = false) {
|
|||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(Icons.Rounded.Psychology, null, tint = accent.primary, modifier = Modifier.size(16.dp))
|
||||
Icon(KaizenIcons.Brain, null, tint = accent.primary, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
stringResource(R.string.stream_reasoning),
|
||||
|
|
@ -85,7 +76,7 @@ fun ReasoningBlock(reasoning: String, streaming: Boolean = false) {
|
|||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
|
||||
if (expanded) KaizenIcons.ChevronUp else KaizenIcons.ChevronDown,
|
||||
null, tint = cs.onSurfaceVariant, modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
|
|
@ -134,21 +125,21 @@ fun ToolEventsBlock(tools: List<ToolEvent>, streaming: Boolean = false) {
|
|||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (analyzing != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Rounded.Description, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
|
||||
Icon(KaizenIcons.FileText, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.stream_tool_analyzing, analyzing!!), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
if (thinking && analyzing == null && completed.isEmpty() && inProgress.isEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Rounded.AutoAwesome, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
|
||||
Icon(KaizenIcons.Sparkles, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(stringResource(R.string.stream_tool_thinking), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
completed.forEach { tool ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val icon = if (tool.type == "error") Icons.Rounded.Error else Icons.Rounded.Check
|
||||
val icon = if (tool.type == "error") KaizenIcons.AlertCircle else KaizenIcons.Check
|
||||
val tint = if (tool.type == "error") Color(0xFFEF4444) else Color(0xFF10B981)
|
||||
Icon(icon, null, tint = tint, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
|
|
@ -161,7 +152,7 @@ fun ToolEventsBlock(tools: List<ToolEvent>, streaming: Boolean = false) {
|
|||
}
|
||||
inProgress.forEach { name ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Rounded.Build, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
|
||||
Icon(KaizenIcons.Wrench, null, tint = accent.primary.copy(alpha = 0.6f), modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(name, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
|
|
@ -184,7 +175,7 @@ fun SourcesBlock(sources: List<SearchSource>, query: String = "") {
|
|||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(Icons.Rounded.Language, null, tint = cs.onSurfaceVariant.copy(alpha = 0.6f), modifier = Modifier.size(13.dp))
|
||||
Icon(KaizenIcons.Globe, null, tint = cs.onSurfaceVariant.copy(alpha = 0.6f), modifier = Modifier.size(13.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
stringResource(R.string.stream_sources) + " (${sources.size})",
|
||||
|
|
@ -194,7 +185,7 @@ fun SourcesBlock(sources: List<SearchSource>, query: String = "") {
|
|||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(
|
||||
if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
|
||||
if (expanded) KaizenIcons.ChevronUp else KaizenIcons.ChevronDown,
|
||||
null, tint = cs.onSurfaceVariant.copy(alpha = 0.4f), modifier = Modifier.size(12.dp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,6 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Fingerprint
|
||||
import androidx.compose.material.icons.rounded.Key
|
||||
import androidx.compose.material.icons.rounded.PhoneAndroid
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -57,6 +53,7 @@ import dev.kaizen.app.net.AppDevice
|
|||
import dev.kaizen.app.net.ServerConfig
|
||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
@Composable
|
||||
fun SecuritySection(
|
||||
|
|
@ -100,7 +97,7 @@ fun SecuritySection(
|
|||
title = stringResource(R.string.settings_biometric_title),
|
||||
description = if (biometricNotEnrolled) stringResource(R.string.settings_biometric_not_enrolled)
|
||||
else stringResource(R.string.settings_biometric_desc),
|
||||
icon = Icons.Rounded.Fingerprint,
|
||||
icon = KaizenIcons.Fingerprint,
|
||||
) {
|
||||
Switch(
|
||||
checked = biometricEnabled && biometricAvailable,
|
||||
|
|
@ -119,7 +116,7 @@ fun SecuritySection(
|
|||
SettingsOptionRow(
|
||||
title = stringResource(R.string.settings_devices_title),
|
||||
description = stringResource(R.string.settings_devices_desc),
|
||||
icon = Icons.Rounded.PhoneAndroid,
|
||||
icon = KaizenIcons.Smartphone,
|
||||
) {
|
||||
if (devicesLoading) {
|
||||
CircularProgressIndicator(
|
||||
|
|
@ -152,7 +149,7 @@ fun SecuritySection(
|
|||
SettingsOptionRow(
|
||||
title = stringResource(R.string.settings_password_title),
|
||||
description = "",
|
||||
icon = Icons.Rounded.Key,
|
||||
icon = KaizenIcons.Key,
|
||||
) {
|
||||
PasswordChangeForm(
|
||||
onSubmit = { current, new_ ->
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ import dev.kaizen.app.ui.effect.KaizenShadows
|
|||
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -57,6 +55,7 @@ import androidx.compose.ui.unit.sp
|
|||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||
import dev.kaizen.app.ui.theme.Oklab
|
||||
import dev.kaizen.app.ui.theme.themes.KaizenThemeId
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
/** Converts polar OKLCH coordinates (Lightness, Chroma, Hue in degrees) to a standard Color */
|
||||
private fun oklch(l: Float, c: Float, h: Float, alpha: Float = 1.0f): Color {
|
||||
|
|
@ -250,7 +249,7 @@ fun ThemeOptionPill(
|
|||
if (selected) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
imageVector = KaizenIcons.Check,
|
||||
contentDescription = null,
|
||||
tint = if (themeId == KaizenThemeId.MONO && isDark.not()) Color.White else Color.Black,
|
||||
modifier = Modifier.size(16.dp)
|
||||
|
|
|
|||
|
|
@ -22,13 +22,6 @@ import dev.kaizen.app.ui.effect.GlassTiers
|
|||
import dev.kaizen.app.ui.effect.KaizenShadows
|
||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.AutoAwesome
|
||||
import androidx.compose.material.icons.rounded.Language
|
||||
import androidx.compose.material.icons.rounded.Palette
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -47,6 +40,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dev.kaizen.app.chat.MeshBackground
|
||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||
|
||||
/**
|
||||
* Premium, fully responsive, glassmorphic Settings Screen.
|
||||
|
|
@ -95,7 +89,7 @@ fun SettingsScreen(
|
|||
modifier = Modifier.size(44.dp).clickable { onBack() },
|
||||
) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(Icons.AutoMirrored.Rounded.ArrowBack, stringResource(R.string.settings_back), tint = cs.onBackground, modifier = Modifier.size(20.dp))
|
||||
Icon(KaizenIcons.ArrowLeft, stringResource(R.string.settings_back), tint = cs.onBackground, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +101,7 @@ fun SettingsScreen(
|
|||
Text(stringResource(R.string.settings_section_appearance), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp, bottom = 8.dp))
|
||||
|
||||
SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) {
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_accent_title), description = stringResource(R.string.settings_accent_desc), icon = Icons.Rounded.Palette) {
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_accent_title), description = stringResource(R.string.settings_accent_desc), icon = KaizenIcons.Palette) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
KaizenThemeId.entries.forEach { theme ->
|
||||
ThemeOptionPill(themeId = theme, selected = selectedTheme == theme, onClick = { viewModel.selectTheme(theme) })
|
||||
|
|
@ -115,7 +109,7 @@ fun SettingsScreen(
|
|||
}
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_appearance_title), description = stringResource(R.string.settings_appearance_desc), icon = Icons.Rounded.Settings) {
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_appearance_title), description = stringResource(R.string.settings_appearance_desc), icon = KaizenIcons.Settings) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
AppAppearance.entries.forEach { mode ->
|
||||
val label = when (mode) {
|
||||
|
|
@ -128,7 +122,7 @@ fun SettingsScreen(
|
|||
}
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_language_title), description = stringResource(R.string.settings_language_desc), icon = Icons.Rounded.Language) {
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_language_title), description = stringResource(R.string.settings_language_desc), icon = KaizenIcons.Globe) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
listOf("system" to R.string.settings_language_system, "de" to R.string.settings_language_de, "en" to R.string.settings_language_en).forEach { (code, labelRes) ->
|
||||
val ctx = LocalContext.current
|
||||
|
|
@ -141,17 +135,17 @@ fun SettingsScreen(
|
|||
Text(stringResource(R.string.settings_section_profile), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp, bottom = 8.dp))
|
||||
|
||||
SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) {
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_name_title), description = stringResource(R.string.settings_name_desc), icon = Icons.Rounded.Person) {
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_name_title), description = stringResource(R.string.settings_name_desc), icon = KaizenIcons.User) {
|
||||
SettingsInputCapsule(value = userName, onValueChange = { viewModel.updateUserName(it) })
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_email_title), description = userEmail, icon = Icons.Rounded.Settings)
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_email_title), description = userEmail, icon = KaizenIcons.Settings)
|
||||
}
|
||||
|
||||
Text(stringResource(R.string.settings_section_ai), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp, bottom = 8.dp))
|
||||
|
||||
SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) {
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_ai_title), description = "", icon = Icons.Rounded.AutoAwesome)
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_ai_title), description = "", icon = KaizenIcons.Sparkles)
|
||||
}
|
||||
|
||||
SecuritySection(viewModel = viewModel, config = config)
|
||||
|
|
|
|||
98
app/src/main/java/dev/kaizen/app/ui/icon/KaizenIcons.kt
Normal file
98
app/src/main/java/dev/kaizen/app/ui/icon/KaizenIcons.kt
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package dev.kaizen.app.ui.icon
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.composables.icons.lucide.*
|
||||
|
||||
/**
|
||||
* Central icon registry for the entire app. Every UI component references icons
|
||||
* through this object — never directly from Material, Lucide, or any other
|
||||
* library. This gives us:
|
||||
*
|
||||
* 1. **One-line swap**: change the backing icon here, every usage updates
|
||||
* 2. **Library independence**: migrate from Lucide to custom SVGs gradually
|
||||
* 3. **Semantic naming**: `KaizenIcons.Send` not `Icons.Rounded.Send`
|
||||
* 4. **Brand layer**: the [Custom] section holds hand-drawn Kaizen originals
|
||||
* that override the base library for brand-critical touchpoints
|
||||
*
|
||||
* Convention: group by function, alphabetize within groups.
|
||||
* When adding a custom icon, keep the Lucide fallback as a comment.
|
||||
*/
|
||||
object KaizenIcons {
|
||||
|
||||
// ── Navigation ──────────────────────────────────────────────────────────
|
||||
val ArrowLeft: ImageVector get() = Lucide.ArrowLeft
|
||||
val ChevronDown: ImageVector get() = Lucide.ChevronDown
|
||||
val ChevronUp: ImageVector get() = Lucide.ChevronUp
|
||||
val Menu: ImageVector get() = Lucide.Menu
|
||||
val MoreVertical: ImageVector get() = Lucide.EllipsisVertical
|
||||
val Settings: ImageVector get() = Lucide.Settings
|
||||
val X: ImageVector get() = Lucide.X
|
||||
|
||||
// ── Chat ────────────────────────────────────────────────────────────────
|
||||
val Mic: ImageVector get() = Lucide.Mic
|
||||
val Paperclip: ImageVector get() = Lucide.Paperclip
|
||||
val Plus: ImageVector get() = Lucide.Plus
|
||||
val Send: ImageVector get() = Lucide.SendHorizontal
|
||||
val Sparkles: ImageVector get() = Lucide.Sparkles
|
||||
val Square: ImageVector get() = Lucide.Square
|
||||
val Zap: ImageVector get() = Lucide.Zap
|
||||
|
||||
// ── Modes ───────────────────────────────────────────────────────────────
|
||||
val Brain: ImageVector get() = Lucide.Brain
|
||||
val CircleDot: ImageVector get() = Lucide.CircleDot
|
||||
val Globe: ImageVector get() = Lucide.Globe
|
||||
val Image: ImageVector get() = Lucide.Image
|
||||
val Music: ImageVector get() = Lucide.Music
|
||||
val SlidersHorizontal: ImageVector get() = Lucide.SlidersHorizontal
|
||||
val Video: ImageVector get() = Lucide.Video
|
||||
|
||||
// ── Conversation management ─────────────────────────────────────────────
|
||||
val Check: ImageVector get() = Lucide.Check
|
||||
val Copy: ImageVector get() = Lucide.Copy
|
||||
val Lock: ImageVector get() = Lucide.Lock
|
||||
val LockOpen: ImageVector get() = Lucide.LockOpen
|
||||
val Pencil: ImageVector get() = Lucide.Pencil
|
||||
val Pin: ImageVector get() = Lucide.Pin
|
||||
val PinOff: ImageVector get() = Lucide.PinOff
|
||||
val RefreshCw: ImageVector get() = Lucide.RefreshCw
|
||||
val Star: ImageVector get() = Lucide.Star
|
||||
val StarOff: ImageVector get() = Lucide.StarOff
|
||||
val Trash: ImageVector get() = Lucide.Trash2
|
||||
|
||||
// ── Files & media ───────────────────────────────────────────────────────
|
||||
val Camera: ImageVector get() = Lucide.Camera
|
||||
val Code: ImageVector get() = Lucide.Code
|
||||
val File: ImageVector get() = Lucide.File
|
||||
val FileAudio: ImageVector get() = Lucide.FileAudio
|
||||
val FileText: ImageVector get() = Lucide.FileText
|
||||
val FileVideo: ImageVector get() = Lucide.FileVideo
|
||||
val FileImage: ImageVector get() = Lucide.FileImage
|
||||
|
||||
// ── Search & reasoning ──────────────────────────────────────────────────
|
||||
val Search: ImageVector get() = Lucide.Search
|
||||
val ExternalLink: ImageVector get() = Lucide.ExternalLink
|
||||
val Wrench: ImageVector get() = Lucide.Wrench
|
||||
val AlertCircle: ImageVector get() = Lucide.CircleAlert
|
||||
|
||||
// ── Settings ────────────────────────────────────────────────────────────
|
||||
val Fingerprint: ImageVector get() = Lucide.Fingerprint
|
||||
val Key: ImageVector get() = Lucide.Key
|
||||
val Languages: ImageVector get() = Lucide.Languages
|
||||
val LogOut: ImageVector get() = Lucide.LogOut
|
||||
val Monitor: ImageVector get() = Lucide.Monitor
|
||||
val Moon: ImageVector get() = Lucide.Moon
|
||||
val Palette: ImageVector get() = Lucide.Palette
|
||||
val Smartphone: ImageVector get() = Lucide.Smartphone
|
||||
val Sun: ImageVector get() = Lucide.Sun
|
||||
val User: ImageVector get() = Lucide.User
|
||||
|
||||
// ── Scroll ──────────────────────────────────────────────────────────────
|
||||
val ArrowDown: ImageVector get() = Lucide.ArrowDown
|
||||
val ArrowUp: ImageVector get() = Lucide.ArrowUp
|
||||
|
||||
// ── Custom (brand overrides) ────────────────────────────────────────────
|
||||
// Add hand-drawn SVGs here as ImageVector literals when ready.
|
||||
// Example:
|
||||
// val Send: ImageVector get() = KaizenCustomIcons.Send
|
||||
// The Lucide fallback above is then removed.
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ kotlinxSerialization = "1.7.3"
|
|||
securityCrypto = "1.1.0-alpha06"
|
||||
room = "2.8.4"
|
||||
biometric = "1.4.0-alpha02"
|
||||
composeIcons = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
|
@ -23,6 +24,7 @@ androidx-security-crypto = { group = "androidx.security", name = "security-crypt
|
|||
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
|
||||
compose-icons-lucide = { group = "com.composables", name = "icons-lucide-android", version.ref = "composeIcons" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue