openuispec 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +52 -34
  2. package/cli/index.ts +1 -1
  3. package/cli/init.ts +48 -211
  4. package/docs/stress-test-maturity-report.md +97 -0
  5. package/examples/todo-orbit/AGENTS.md +127 -0
  6. package/examples/todo-orbit/CLAUDE.md +75 -0
  7. package/examples/todo-orbit/README.md +62 -0
  8. package/examples/todo-orbit/generated/android/Todo Orbit/README.md +14 -0
  9. package/examples/todo-orbit/generated/android/Todo Orbit/app/build.gradle.kts +58 -0
  10. package/examples/todo-orbit/generated/android/Todo Orbit/app/proguard-rules.pro +1 -0
  11. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/AndroidManifest.xml +20 -0
  12. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/MainActivity.kt +14 -0
  13. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/TodoOrbitApp.kt +345 -0
  14. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/AppLogic.kt +231 -0
  15. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Models.kt +169 -0
  16. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/support/Strings.kt +8 -0
  17. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +185 -0
  18. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/AnalyticsScreen.kt +193 -0
  19. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/SettingsScreen.kt +102 -0
  20. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +342 -0
  21. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +344 -0
  22. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/theme/TodoOrbitTheme.kt +59 -0
  23. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +148 -0
  24. package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +154 -0
  25. package/examples/todo-orbit/generated/android/Todo Orbit/build.gradle.kts +4 -0
  26. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.jar +0 -0
  27. package/examples/todo-orbit/generated/android/Todo Orbit/gradle/wrapper/gradle-wrapper.properties +7 -0
  28. package/examples/todo-orbit/generated/android/Todo Orbit/gradle.properties +4 -0
  29. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew +248 -0
  30. package/examples/todo-orbit/generated/android/Todo Orbit/gradlew.bat +93 -0
  31. package/examples/todo-orbit/generated/android/Todo Orbit/settings.gradle.kts +18 -0
  32. package/examples/todo-orbit/generated/ios/Todo Orbit/README.md +29 -0
  33. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +118 -0
  34. package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +118 -0
  35. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/App/TodoOrbitApp.swift +50 -0
  36. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/OrbitChrome.swift +204 -0
  37. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/SchedulePreviewView.swift +126 -0
  38. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Components/TrendChartView.swift +70 -0
  39. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +123 -0
  40. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +60 -0
  41. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Models/DomainModels.swift +238 -0
  42. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/AnalyticsView.swift +94 -0
  43. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +74 -0
  44. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +363 -0
  45. package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +324 -0
  46. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.pbxproj +408 -0
  47. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  48. package/examples/todo-orbit/generated/ios/Todo Orbit/TodoOrbit.xcodeproj/xcshareddata/xcschemes/TodoOrbit.xcscheme +79 -0
  49. package/examples/todo-orbit/generated/ios/Todo Orbit/project.yml +24 -0
  50. package/examples/todo-orbit/generated/web/Todo Orbit/index.html +16 -0
  51. package/examples/todo-orbit/generated/web/Todo Orbit/package-lock.json +1087 -0
  52. package/examples/todo-orbit/generated/web/Todo Orbit/package.json +24 -0
  53. package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +2114 -0
  54. package/examples/todo-orbit/generated/web/Todo Orbit/src/main.tsx +13 -0
  55. package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +886 -0
  56. package/examples/todo-orbit/generated/web/Todo Orbit/tsconfig.json +19 -0
  57. package/examples/todo-orbit/generated/web/Todo Orbit/vite.config.ts +6 -0
  58. package/examples/todo-orbit/openuispec/README.md +158 -0
  59. package/examples/todo-orbit/openuispec/contracts/.gitkeep +0 -0
  60. package/examples/todo-orbit/openuispec/contracts/action_trigger.yaml +28 -0
  61. package/examples/todo-orbit/openuispec/contracts/collection.yaml +32 -0
  62. package/examples/todo-orbit/openuispec/contracts/data_display.yaml +38 -0
  63. package/examples/todo-orbit/openuispec/contracts/feedback.yaml +32 -0
  64. package/examples/todo-orbit/openuispec/contracts/input_field.yaml +52 -0
  65. package/examples/todo-orbit/openuispec/contracts/nav_container.yaml +47 -0
  66. package/examples/todo-orbit/openuispec/contracts/surface.yaml +28 -0
  67. package/examples/todo-orbit/openuispec/contracts/x_schedule_preview.yaml +134 -0
  68. package/examples/todo-orbit/openuispec/contracts/x_task_trend_chart.yaml +139 -0
  69. package/examples/todo-orbit/openuispec/flows/.gitkeep +0 -0
  70. package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +253 -0
  71. package/examples/todo-orbit/openuispec/flows/create_task.yaml +118 -0
  72. package/examples/todo-orbit/openuispec/flows/edit_task.yaml +126 -0
  73. package/examples/todo-orbit/openuispec/locales/.gitkeep +0 -0
  74. package/examples/todo-orbit/openuispec/locales/en.json +150 -0
  75. package/examples/todo-orbit/openuispec/locales/ru.json +150 -0
  76. package/examples/todo-orbit/openuispec/openuispec.yaml +122 -0
  77. package/examples/todo-orbit/openuispec/platform/.gitkeep +0 -0
  78. package/examples/todo-orbit/openuispec/platform/android.yaml +19 -0
  79. package/examples/todo-orbit/openuispec/platform/ios.yaml +20 -0
  80. package/examples/todo-orbit/openuispec/platform/web.yaml +22 -0
  81. package/examples/todo-orbit/openuispec/screens/.gitkeep +0 -0
  82. package/examples/todo-orbit/openuispec/screens/analytics.yaml +140 -0
  83. package/examples/todo-orbit/openuispec/screens/home.yaml +173 -0
  84. package/examples/todo-orbit/openuispec/screens/settings.yaml +149 -0
  85. package/examples/todo-orbit/openuispec/screens/task_detail.yaml +223 -0
  86. package/examples/todo-orbit/openuispec/tokens/.gitkeep +0 -0
  87. package/examples/todo-orbit/openuispec/tokens/color.yaml +93 -0
  88. package/examples/todo-orbit/openuispec/tokens/elevation.yaml +25 -0
  89. package/examples/todo-orbit/openuispec/tokens/icons.yaml +92 -0
  90. package/examples/todo-orbit/openuispec/tokens/layout.yaml +107 -0
  91. package/examples/todo-orbit/openuispec/tokens/motion.yaml +39 -0
  92. package/examples/todo-orbit/openuispec/tokens/spacing.yaml +18 -0
  93. package/examples/todo-orbit/openuispec/tokens/themes.yaml +23 -0
  94. package/examples/todo-orbit/openuispec/tokens/typography.yaml +52 -0
  95. package/package.json +1 -1
  96. package/schema/screen.schema.json +9 -0
  97. package/schema/validate.ts +0 -2
  98. package/spec/openuispec-v0.1.md +129 -27
@@ -0,0 +1,169 @@
1
+ package uz.rsteam.todoorbit
2
+
3
+ import androidx.compose.material3.MaterialTheme
4
+ import androidx.compose.runtime.Composable
5
+ import androidx.compose.runtime.saveable.Saver
6
+ import androidx.compose.runtime.saveable.listSaver
7
+ import androidx.compose.ui.res.stringResource
8
+ import java.time.LocalDate
9
+ import java.time.LocalDateTime
10
+
11
+ enum class ScreenTab { Tasks, Analytics, Settings }
12
+
13
+ enum class UiLocale { En, Ru }
14
+
15
+ enum class ThemeMode { Light, Dark }
16
+
17
+ enum class TaskStatus { Open, Done }
18
+
19
+ enum class Priority { Low, Medium, High }
20
+
21
+ enum class TaskFilter { All, Open, Done }
22
+
23
+ enum class AnalyticsPeriod { Week, Month, Quarter }
24
+
25
+ enum class Cadence { Daily, Weekly, Monthly }
26
+
27
+ enum class Weekday { Mon, Tue, Wed, Thu, Fri, Sat, Sun }
28
+
29
+ enum class SummaryChannel { Push, Email }
30
+
31
+ enum class PreviewMode { Invalid, Empty, Ready }
32
+
33
+ sealed interface OverlayState {
34
+ data object CreateTask : OverlayState
35
+ data class EditTask(val taskId: String) : OverlayState
36
+ data object RecurringRule : OverlayState
37
+ data class TaskMeta(val taskId: String) : OverlayState
38
+ }
39
+
40
+ data class PreferencesState(
41
+ val locale: UiLocale = UiLocale.En,
42
+ val themeMode: ThemeMode = ThemeMode.Light,
43
+ val remindersEnabled: Boolean = true,
44
+ val dailySummaryEnabled: Boolean = false
45
+ )
46
+
47
+ val PreferencesStateSaver: Saver<PreferencesState, Any> = listSaver(
48
+ save = {
49
+ listOf(
50
+ it.locale.name,
51
+ it.themeMode.name,
52
+ it.remindersEnabled,
53
+ it.dailySummaryEnabled
54
+ )
55
+ },
56
+ restore = {
57
+ PreferencesState(
58
+ locale = UiLocale.valueOf(it[0] as String),
59
+ themeMode = ThemeMode.valueOf(it[1] as String),
60
+ remindersEnabled = it[2] as Boolean,
61
+ dailySummaryEnabled = it[3] as Boolean
62
+ )
63
+ }
64
+ )
65
+
66
+ data class TaskModel(
67
+ val id: String,
68
+ val title: String,
69
+ val notes: String = "",
70
+ val status: TaskStatus,
71
+ val priority: Priority,
72
+ val dueDate: LocalDate? = null,
73
+ val createdAt: LocalDateTime,
74
+ val updatedAt: LocalDateTime
75
+ )
76
+
77
+ data class RecurringRuleModel(
78
+ val id: String,
79
+ val name: String,
80
+ val cadence: Cadence,
81
+ val interval: Int,
82
+ val weekday: Weekday? = null,
83
+ val monthDay: Int? = null,
84
+ val startDate: LocalDate,
85
+ val endDate: LocalDate? = null,
86
+ val remindAt: String? = null,
87
+ val summaryChannel: SummaryChannel? = null
88
+ )
89
+
90
+ data class TrendPoint(
91
+ val label: String,
92
+ val completed: Int,
93
+ val created: Int
94
+ )
95
+
96
+ data class TaskDraft(
97
+ val title: String = "",
98
+ val notes: String = "",
99
+ val priority: Priority = Priority.Medium,
100
+ val dueDate: LocalDate? = null
101
+ )
102
+
103
+ data class RuleDraft(
104
+ val name: String = "",
105
+ val confirmName: String = "",
106
+ val cadence: Cadence? = null,
107
+ val interval: String = "1",
108
+ val weekday: Weekday? = null,
109
+ val monthDay: String = "",
110
+ val startDate: LocalDate = LocalDate.now(),
111
+ val hasEndDate: Boolean = false,
112
+ val endDate: LocalDate? = null,
113
+ val remindAt: String = "",
114
+ val enableSummary: Boolean = false,
115
+ val summaryChannel: SummaryChannel? = null
116
+ )
117
+
118
+ data class SchedulePreviewState(
119
+ val mode: PreviewMode,
120
+ val occurrences: List<LocalDate> = emptyList()
121
+ )
122
+
123
+ sealed interface UiText {
124
+ data class Resource(
125
+ val id: Int,
126
+ val args: List<Any> = emptyList()
127
+ ) : UiText
128
+
129
+ data class Literal(val value: String) : UiText
130
+ }
131
+
132
+ val TaskStatus.labelRes: Int
133
+ get() = when (this) {
134
+ TaskStatus.Open -> R.string.status_open
135
+ TaskStatus.Done -> R.string.status_done
136
+ }
137
+
138
+ val Priority.labelRes: Int
139
+ get() = when (this) {
140
+ Priority.Low -> R.string.priority_low
141
+ Priority.Medium -> R.string.priority_medium
142
+ Priority.High -> R.string.priority_high
143
+ }
144
+
145
+ val Weekday.labelRes: Int
146
+ get() = when (this) {
147
+ Weekday.Mon -> R.string.weekday_mon
148
+ Weekday.Tue -> R.string.weekday_tue
149
+ Weekday.Wed -> R.string.weekday_wed
150
+ Weekday.Thu -> R.string.weekday_thu
151
+ Weekday.Fri -> R.string.weekday_fri
152
+ Weekday.Sat -> R.string.weekday_sat
153
+ Weekday.Sun -> R.string.weekday_sun
154
+ }
155
+
156
+ val Priority.color
157
+ @Composable get() = when (this) {
158
+ Priority.Low -> MaterialTheme.colorScheme.outline
159
+ Priority.Medium -> MaterialTheme.colorScheme.tertiary
160
+ Priority.High -> MaterialTheme.colorScheme.secondary
161
+ }
162
+
163
+ @Composable
164
+ fun UiText.resolve(): String {
165
+ return when (this) {
166
+ is UiText.Resource -> stringResource(id, *args.toTypedArray())
167
+ is UiText.Literal -> value
168
+ }
169
+ }
@@ -0,0 +1,8 @@
1
+ package uz.rsteam.todoorbit
2
+
3
+ fun UiLocale.toLanguageTag(): String {
4
+ return when (this) {
5
+ UiLocale.En -> "en"
6
+ UiLocale.Ru -> "ru"
7
+ }
8
+ }
@@ -0,0 +1,185 @@
1
+ package uz.rsteam.todoorbit
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.Column
7
+ import androidx.compose.foundation.layout.ExperimentalLayoutApi
8
+ import androidx.compose.foundation.layout.FlowRow
9
+ import androidx.compose.foundation.layout.Row
10
+ import androidx.compose.foundation.layout.Spacer
11
+ import androidx.compose.foundation.layout.fillMaxWidth
12
+ import androidx.compose.foundation.layout.padding
13
+ import androidx.compose.foundation.layout.size
14
+ import androidx.compose.foundation.layout.width
15
+ import androidx.compose.foundation.shape.CircleShape
16
+ import androidx.compose.material.icons.Icons
17
+ import androidx.compose.material.icons.outlined.Translate
18
+ import androidx.compose.material3.Card
19
+ import androidx.compose.material3.CardDefaults
20
+ import androidx.compose.material3.ElevatedCard
21
+ import androidx.compose.material3.FilterChip
22
+ import androidx.compose.material3.Icon
23
+ import androidx.compose.material3.MaterialTheme
24
+ import androidx.compose.material3.OutlinedCard
25
+ import androidx.compose.material3.OutlinedTextField
26
+ import androidx.compose.material3.Switch
27
+ import androidx.compose.material3.Text
28
+ import androidx.compose.runtime.Composable
29
+ import androidx.compose.ui.Alignment
30
+ import androidx.compose.ui.Modifier
31
+ import androidx.compose.ui.graphics.Color
32
+ import androidx.compose.ui.res.stringResource
33
+ import androidx.compose.ui.text.font.FontWeight
34
+ import androidx.compose.ui.unit.dp
35
+
36
+ @Composable
37
+ fun HeroCard(title: String, subtitle: String) {
38
+ ElevatedCard(
39
+ shape = MaterialTheme.shapes.large,
40
+ colors = CardDefaults.elevatedCardColors(
41
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f)
42
+ )
43
+ ) {
44
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
45
+ Text(title, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
46
+ Text(subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
47
+ }
48
+ }
49
+ }
50
+
51
+ @Composable
52
+ fun StatCard(title: String, value: String) {
53
+ Card(shape = MaterialTheme.shapes.medium) {
54
+ Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
55
+ Text(title, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
56
+ Text(value, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
57
+ }
58
+ }
59
+ }
60
+
61
+ @Composable
62
+ fun LegendDot(color: Color, label: String) {
63
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
64
+ Box(modifier = Modifier.size(10.dp).background(color, CircleShape))
65
+ Text(label, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
66
+ }
67
+ }
68
+
69
+ @Composable
70
+ fun LanguageSelector(current: UiLocale, onSelected: (UiLocale) -> Unit) {
71
+ EnumSelector(
72
+ title = stringResource(R.string.settings_language),
73
+ current = current.name,
74
+ options = listOf(
75
+ UiLocale.En.name to stringResource(R.string.settings_language_en),
76
+ UiLocale.Ru.name to stringResource(R.string.settings_language_ru)
77
+ ),
78
+ leadingIcon = {
79
+ Icon(Icons.Outlined.Translate, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
80
+ },
81
+ onSelected = { selected -> onSelected(UiLocale.entries.first { it.name == selected }) }
82
+ )
83
+ }
84
+
85
+ @Composable
86
+ fun ThemeSelector(current: ThemeMode, onSelected: (ThemeMode) -> Unit) {
87
+ EnumSelector(
88
+ title = stringResource(R.string.settings_theme),
89
+ current = current.name,
90
+ options = listOf(
91
+ ThemeMode.Light.name to stringResource(R.string.settings_theme_light),
92
+ ThemeMode.Dark.name to stringResource(R.string.settings_theme_dark)
93
+ ),
94
+ onSelected = { selected -> onSelected(ThemeMode.entries.first { it.name == selected }) }
95
+ )
96
+ }
97
+
98
+ @OptIn(ExperimentalLayoutApi::class)
99
+ @Composable
100
+ fun EnumSelector(
101
+ title: String,
102
+ current: String,
103
+ options: List<Pair<String, String>>,
104
+ leadingIcon: @Composable (() -> Unit)? = null,
105
+ onSelected: (String) -> Unit
106
+ ) {
107
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
108
+ Text(title, style = MaterialTheme.typography.titleSmall)
109
+ FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
110
+ options.forEach { (value, label) ->
111
+ FilterChip(
112
+ selected = value == current,
113
+ onClick = { onSelected(value) },
114
+ label = { Text(label) },
115
+ leadingIcon = if (value == current && leadingIcon != null) leadingIcon else null
116
+ )
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ @Composable
123
+ fun SettingsToggle(
124
+ title: String,
125
+ subtitle: String,
126
+ checked: Boolean,
127
+ onCheckedChange: (Boolean) -> Unit
128
+ ) {
129
+ Row(
130
+ modifier = Modifier.fillMaxWidth(),
131
+ horizontalArrangement = Arrangement.SpaceBetween,
132
+ verticalAlignment = Alignment.CenterVertically
133
+ ) {
134
+ Column(modifier = Modifier.fillMaxWidth(0.8f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
135
+ Text(title, fontWeight = FontWeight.SemiBold)
136
+ Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
137
+ }
138
+ Spacer(Modifier.width(12.dp))
139
+ Switch(checked = checked, onCheckedChange = onCheckedChange)
140
+ }
141
+ }
142
+
143
+ @Composable
144
+ fun RuleTextField(
145
+ label: String,
146
+ value: String,
147
+ placeholder: String = "",
148
+ helper: String? = null,
149
+ error: String? = null,
150
+ onValueChange: (String) -> Unit
151
+ ) {
152
+ Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
153
+ OutlinedTextField(
154
+ value = value,
155
+ onValueChange = onValueChange,
156
+ modifier = Modifier.fillMaxWidth(),
157
+ label = { Text(label) },
158
+ placeholder = if (placeholder.isNotBlank()) ({ Text(placeholder) }) else null,
159
+ shape = MaterialTheme.shapes.medium,
160
+ isError = error != null
161
+ )
162
+ when {
163
+ error != null -> Text(error, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
164
+ helper != null -> Text(helper, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall)
165
+ }
166
+ }
167
+ }
168
+
169
+ @Composable
170
+ fun EmptyCard(title: String, body: String) {
171
+ OutlinedCard(shape = MaterialTheme.shapes.large) {
172
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
173
+ Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
174
+ Text(body, color = MaterialTheme.colorScheme.onSurfaceVariant)
175
+ }
176
+ }
177
+ }
178
+
179
+ @Composable
180
+ fun LabelValue(label: String, value: String) {
181
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
182
+ Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant)
183
+ Text(value, fontWeight = FontWeight.SemiBold)
184
+ }
185
+ }
@@ -0,0 +1,193 @@
1
+ package uz.rsteam.todoorbit
2
+
3
+ import androidx.compose.foundation.Canvas
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Column
6
+ import androidx.compose.foundation.layout.ExperimentalLayoutApi
7
+ import androidx.compose.foundation.layout.FlowRow
8
+ import androidx.compose.foundation.layout.PaddingValues
9
+ import androidx.compose.foundation.layout.Row
10
+ import androidx.compose.foundation.layout.fillMaxSize
11
+ import androidx.compose.foundation.layout.fillMaxWidth
12
+ import androidx.compose.foundation.layout.height
13
+ import androidx.compose.foundation.layout.padding
14
+ import androidx.compose.foundation.lazy.LazyColumn
15
+ import androidx.compose.foundation.lazy.grid.GridCells
16
+ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
17
+ import androidx.compose.material3.ElevatedCard
18
+ import androidx.compose.material3.FilterChip
19
+ import androidx.compose.material3.MaterialTheme
20
+ import androidx.compose.material3.Text
21
+ import androidx.compose.runtime.Composable
22
+ import androidx.compose.ui.Modifier
23
+ import androidx.compose.ui.geometry.Offset
24
+ import androidx.compose.ui.graphics.Path
25
+ import androidx.compose.ui.graphics.StrokeCap
26
+ import androidx.compose.ui.graphics.drawscope.Stroke
27
+ import androidx.compose.ui.res.stringResource
28
+ import androidx.compose.ui.text.font.FontWeight
29
+ import androidx.compose.ui.unit.dp
30
+
31
+ @OptIn(ExperimentalLayoutApi::class)
32
+ @Composable
33
+ fun AnalyticsScreen(
34
+ period: AnalyticsPeriod,
35
+ overview: AnalyticsOverview,
36
+ trendSeries: List<TrendPoint>,
37
+ overdueTasks: List<TaskModel>,
38
+ onPeriodChange: (AnalyticsPeriod) -> Unit,
39
+ locale: UiLocale
40
+ ) {
41
+ LazyColumn(
42
+ modifier = Modifier.fillMaxSize(),
43
+ verticalArrangement = Arrangement.spacedBy(16.dp),
44
+ contentPadding = PaddingValues(20.dp)
45
+ ) {
46
+ item {
47
+ HeroCard(
48
+ stringResource(R.string.analytics_title),
49
+ stringResource(R.string.analytics_subtitle)
50
+ )
51
+ }
52
+ item {
53
+ FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
54
+ AnalyticsPeriod.entries.forEach { entry ->
55
+ FilterChip(
56
+ selected = period == entry,
57
+ onClick = { onPeriodChange(entry) },
58
+ label = {
59
+ Text(
60
+ when (entry) {
61
+ AnalyticsPeriod.Week -> stringResource(R.string.analytics_period_week)
62
+ AnalyticsPeriod.Month -> stringResource(R.string.analytics_period_month)
63
+ AnalyticsPeriod.Quarter -> stringResource(R.string.analytics_period_quarter)
64
+ }
65
+ )
66
+ }
67
+ )
68
+ }
69
+ }
70
+ }
71
+ item {
72
+ LazyVerticalGrid(
73
+ columns = GridCells.Fixed(2),
74
+ modifier = Modifier.height(220.dp),
75
+ verticalArrangement = Arrangement.spacedBy(12.dp),
76
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
77
+ userScrollEnabled = false
78
+ ) {
79
+ item { StatCard(stringResource(R.string.analytics_completed_today), overview.completedToday.toString()) }
80
+ item { StatCard(stringResource(R.string.analytics_open_tasks), overview.openTasks.toString()) }
81
+ item { StatCard(stringResource(R.string.analytics_overdue_tasks), overview.overdueTasks.toString()) }
82
+ item { StatCard(stringResource(R.string.analytics_completion_rate), "${overview.completionRate}%") }
83
+ }
84
+ }
85
+ item {
86
+ TrendChartCard(series = trendSeries, period = period)
87
+ }
88
+ item {
89
+ ElevatedCard(shape = MaterialTheme.shapes.large) {
90
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
91
+ Text(stringResource(R.string.analytics_overdue_section), style = MaterialTheme.typography.headlineSmall)
92
+ Text(stringResource(R.string.analytics_overdue_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant)
93
+ if (overdueTasks.isEmpty()) {
94
+ Text(stringResource(R.string.analytics_empty_overdue_body), color = MaterialTheme.colorScheme.onSurfaceVariant)
95
+ } else {
96
+ overdueTasks.forEach { task ->
97
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
98
+ Column {
99
+ Text(task.title, fontWeight = FontWeight.SemiBold)
100
+ Text(stringResource(task.priority.labelRes), color = MaterialTheme.colorScheme.onSurfaceVariant)
101
+ }
102
+ Text(task.dueDate?.let { formatAbsolute(it, locale) }.orEmpty())
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ @Composable
113
+ fun TrendChartCard(
114
+ series: List<TrendPoint>,
115
+ period: AnalyticsPeriod
116
+ ) {
117
+ val outlineVariant = MaterialTheme.colorScheme.outlineVariant
118
+ val createdColor = MaterialTheme.colorScheme.secondary
119
+ val completedColor = MaterialTheme.colorScheme.primary
120
+
121
+ ElevatedCard(shape = MaterialTheme.shapes.large) {
122
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
123
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
124
+ Column {
125
+ Text(
126
+ stringResource(R.string.analytics_contract_label),
127
+ style = MaterialTheme.typography.labelSmall,
128
+ color = MaterialTheme.colorScheme.onSurfaceVariant
129
+ )
130
+ Text(
131
+ when (period) {
132
+ AnalyticsPeriod.Week -> stringResource(R.string.analytics_period_week)
133
+ AnalyticsPeriod.Month -> stringResource(R.string.analytics_period_month)
134
+ AnalyticsPeriod.Quarter -> stringResource(R.string.analytics_period_quarter)
135
+ },
136
+ style = MaterialTheme.typography.titleLarge
137
+ )
138
+ }
139
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
140
+ LegendDot(color = createdColor, label = stringResource(R.string.analytics_legend_created))
141
+ LegendDot(color = completedColor, label = stringResource(R.string.analytics_legend_completed))
142
+ }
143
+ }
144
+ if (series.isEmpty()) {
145
+ Text(stringResource(R.string.analytics_empty_trend), color = MaterialTheme.colorScheme.onSurfaceVariant)
146
+ } else {
147
+ Canvas(modifier = Modifier.fillMaxWidth().height(220.dp)) {
148
+ val maxValue = series.maxOf { maxOf(it.completed, it.created) }.coerceAtLeast(1)
149
+ val leftPad = 32.dp.toPx()
150
+ val bottomPad = 28.dp.toPx()
151
+ val usableWidth = size.width - leftPad * 2
152
+ val usableHeight = size.height - bottomPad - 20.dp.toPx()
153
+ repeat(4) { index ->
154
+ val y = 20.dp.toPx() + (usableHeight / 3f) * index
155
+ drawLine(
156
+ color = outlineVariant,
157
+ start = Offset(leftPad, y),
158
+ end = Offset(size.width - leftPad, y)
159
+ )
160
+ }
161
+ fun buildPath(selector: (TrendPoint) -> Int): Path {
162
+ return Path().apply {
163
+ series.forEachIndexed { index, point ->
164
+ val x = leftPad + (usableWidth / series.lastIndex.coerceAtLeast(1)) * index
165
+ val y = 20.dp.toPx() + usableHeight - (usableHeight * selector(point) / maxValue.toFloat())
166
+ if (index == 0) moveTo(x, y) else lineTo(x, y)
167
+ }
168
+ }
169
+ }
170
+ drawPath(
171
+ path = buildPath { it.created },
172
+ color = createdColor,
173
+ style = Stroke(width = 6f, cap = StrokeCap.Round)
174
+ )
175
+ drawPath(
176
+ path = buildPath { it.completed },
177
+ color = completedColor,
178
+ style = Stroke(width = 6f, cap = StrokeCap.Round)
179
+ )
180
+ }
181
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
182
+ series.forEach { point ->
183
+ Text(
184
+ text = point.label,
185
+ style = MaterialTheme.typography.labelSmall,
186
+ color = MaterialTheme.colorScheme.onSurfaceVariant
187
+ )
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,102 @@
1
+ package uz.rsteam.todoorbit
2
+
3
+ import androidx.compose.foundation.layout.Arrangement
4
+ import androidx.compose.foundation.layout.Column
5
+ import androidx.compose.foundation.layout.PaddingValues
6
+ import androidx.compose.foundation.layout.Spacer
7
+ import androidx.compose.foundation.layout.fillMaxSize
8
+ import androidx.compose.foundation.layout.padding
9
+ import androidx.compose.foundation.layout.width
10
+ import androidx.compose.foundation.lazy.LazyColumn
11
+ import androidx.compose.material.icons.Icons
12
+ import androidx.compose.material.icons.outlined.CalendarMonth
13
+ import androidx.compose.material3.Button
14
+ import androidx.compose.material3.ElevatedCard
15
+ import androidx.compose.material3.Icon
16
+ import androidx.compose.material3.MaterialTheme
17
+ import androidx.compose.material3.OutlinedCard
18
+ import androidx.compose.material3.Text
19
+ import androidx.compose.runtime.Composable
20
+ import androidx.compose.ui.Modifier
21
+ import androidx.compose.ui.res.stringResource
22
+ import androidx.compose.ui.text.font.FontWeight
23
+ import androidx.compose.ui.unit.dp
24
+
25
+ @Composable
26
+ fun SettingsScreen(
27
+ preferences: PreferencesState,
28
+ recurringRules: List<RecurringRuleModel>,
29
+ onPreferencesChange: (PreferencesState) -> Unit,
30
+ onOpenRecurringRule: () -> Unit,
31
+ locale: UiLocale
32
+ ) {
33
+ LazyColumn(
34
+ modifier = Modifier.fillMaxSize(),
35
+ verticalArrangement = Arrangement.spacedBy(16.dp),
36
+ contentPadding = PaddingValues(20.dp)
37
+ ) {
38
+ item {
39
+ HeroCard(
40
+ stringResource(R.string.settings_title),
41
+ stringResource(R.string.settings_subtitle)
42
+ )
43
+ }
44
+ item {
45
+ ElevatedCard(shape = MaterialTheme.shapes.large) {
46
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
47
+ LanguageSelector(
48
+ current = preferences.locale,
49
+ onSelected = { onPreferencesChange(preferences.copy(locale = it)) }
50
+ )
51
+ ThemeSelector(
52
+ current = preferences.themeMode,
53
+ onSelected = { onPreferencesChange(preferences.copy(themeMode = it)) }
54
+ )
55
+ SettingsToggle(
56
+ title = stringResource(R.string.settings_reminders),
57
+ subtitle = stringResource(R.string.settings_reminders_helper),
58
+ checked = preferences.remindersEnabled,
59
+ onCheckedChange = { onPreferencesChange(preferences.copy(remindersEnabled = it)) }
60
+ )
61
+ SettingsToggle(
62
+ title = stringResource(R.string.settings_daily_summary),
63
+ subtitle = stringResource(R.string.settings_daily_summary_helper),
64
+ checked = preferences.dailySummaryEnabled,
65
+ onCheckedChange = { onPreferencesChange(preferences.copy(dailySummaryEnabled = it)) }
66
+ )
67
+ }
68
+ }
69
+ }
70
+ item {
71
+ ElevatedCard(shape = MaterialTheme.shapes.large) {
72
+ Column(modifier = Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
73
+ Text(stringResource(R.string.settings_automation_title), style = MaterialTheme.typography.titleLarge)
74
+ Text(stringResource(R.string.settings_automation_subtitle), color = MaterialTheme.colorScheme.onSurfaceVariant)
75
+ Button(onClick = onOpenRecurringRule, shape = MaterialTheme.shapes.medium) {
76
+ Icon(Icons.Outlined.CalendarMonth, contentDescription = null)
77
+ Spacer(Modifier.width(8.dp))
78
+ Text(stringResource(R.string.settings_automation_create_rule))
79
+ }
80
+ recurringRules.forEach { rule ->
81
+ OutlinedCard(shape = MaterialTheme.shapes.medium) {
82
+ Column(modifier = Modifier.padding(16.dp)) {
83
+ Text(rule.name, fontWeight = FontWeight.SemiBold)
84
+ Text(ruleDescription(rule, locale), color = MaterialTheme.colorScheme.onSurfaceVariant)
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ @Composable
95
+ private fun ruleDescription(rule: RecurringRuleModel, locale: UiLocale): String {
96
+ val cadence = when (rule.cadence) {
97
+ Cadence.Daily -> stringResource(R.string.recurring_rule_cadence_daily)
98
+ Cadence.Weekly -> "${stringResource(R.string.recurring_rule_cadence_weekly)} · ${stringResource(rule.weekday!!.labelRes)}"
99
+ Cadence.Monthly -> "${stringResource(R.string.recurring_rule_cadence_monthly)} · ${rule.monthDay}"
100
+ }
101
+ return "$cadence · ${formatAbsolute(rule.startDate, locale)}"
102
+ }