openuispec 0.1.27 → 0.1.29
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.
- package/README.md +52 -55
- package/cli/configure-target.ts +416 -0
- package/cli/index.ts +14 -3
- package/cli/init.ts +241 -55
- package/cli/target-presets.json +746 -0
- package/docs/implementation-notes.md +47 -10
- package/docs/release-notes-v0.1.26.md +1 -1
- package/docs/release-notes-v0.1.28.md +25 -0
- package/docs/stress-test-maturity-report.md +1 -1
- package/drift/index.ts +31 -11
- package/examples/taskflow/AGENTS.md +113 -0
- package/examples/taskflow/CLAUDE.md +113 -0
- package/examples/taskflow/backend/.gitkeep +1 -0
- package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
- package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
- package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
- package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
- package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
- package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
- package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
- package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
- package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
- package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
- package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
- package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
- package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
- package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
- package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
- package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
- package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
- package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
- package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
- package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
- package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
- package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
- package/examples/taskflow/openuispec/README.md +54 -0
- package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +2 -0
- package/examples/todo-orbit/AGENTS.md +48 -22
- package/examples/todo-orbit/CLAUDE.md +48 -22
- package/examples/todo-orbit/backend/.gitkeep +1 -0
- package/examples/todo-orbit/openuispec/README.md +9 -4
- package/examples/todo-orbit/openuispec/openuispec.yaml +2 -0
- package/package.json +1 -1
- package/prepare/index.ts +811 -25
- package/schema/openuispec.schema.json +10 -0
- package/schema/semantic-lint.ts +36 -12
- package/schema/validate.ts +9 -4
- package/status/index.ts +16 -3
- /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
- /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/typography.yaml +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
package uz.rsteam.taskflow
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
4
|
+
import androidx.compose.foundation.layout.Row
|
|
5
|
+
import androidx.compose.foundation.layout.fillMaxHeight
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
7
|
+
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
8
|
+
import androidx.compose.material.icons.Icons
|
|
9
|
+
import androidx.compose.material.icons.outlined.CalendarMonth
|
|
10
|
+
import androidx.compose.material.icons.outlined.Folder
|
|
11
|
+
import androidx.compose.material.icons.outlined.Home
|
|
12
|
+
import androidx.compose.material.icons.outlined.Settings
|
|
13
|
+
import androidx.compose.material3.FloatingActionButton
|
|
14
|
+
import androidx.compose.material3.Icon
|
|
15
|
+
import androidx.compose.material3.MaterialTheme
|
|
16
|
+
import androidx.compose.material3.NavigationBar
|
|
17
|
+
import androidx.compose.material3.NavigationBarItem
|
|
18
|
+
import androidx.compose.material3.NavigationRail
|
|
19
|
+
import androidx.compose.material3.NavigationRailItem
|
|
20
|
+
import androidx.compose.material3.Scaffold
|
|
21
|
+
import androidx.compose.material3.Text
|
|
22
|
+
import androidx.compose.runtime.Composable
|
|
23
|
+
import androidx.compose.runtime.getValue
|
|
24
|
+
import androidx.compose.runtime.mutableStateListOf
|
|
25
|
+
import androidx.compose.runtime.mutableStateOf
|
|
26
|
+
import androidx.compose.runtime.remember
|
|
27
|
+
import androidx.compose.runtime.setValue
|
|
28
|
+
import androidx.compose.ui.Modifier
|
|
29
|
+
import androidx.compose.ui.unit.dp
|
|
30
|
+
import uz.rsteam.taskflow.model.NewProjectDraft
|
|
31
|
+
import uz.rsteam.taskflow.model.Priority
|
|
32
|
+
import uz.rsteam.taskflow.model.Project
|
|
33
|
+
import uz.rsteam.taskflow.model.Route
|
|
34
|
+
import uz.rsteam.taskflow.model.SampleData
|
|
35
|
+
import uz.rsteam.taskflow.model.SheetState
|
|
36
|
+
import uz.rsteam.taskflow.model.Task
|
|
37
|
+
import uz.rsteam.taskflow.model.TaskEditorDraft
|
|
38
|
+
import uz.rsteam.taskflow.model.TaskFilter
|
|
39
|
+
import uz.rsteam.taskflow.model.TaskStatus
|
|
40
|
+
import uz.rsteam.taskflow.model.TopLevelScreen
|
|
41
|
+
import uz.rsteam.taskflow.ui.screens.CalendarScreen
|
|
42
|
+
import uz.rsteam.taskflow.ui.screens.HomeScreen
|
|
43
|
+
import uz.rsteam.taskflow.ui.screens.ProfileEditScreen
|
|
44
|
+
import uz.rsteam.taskflow.ui.screens.ProjectDetailScreen
|
|
45
|
+
import uz.rsteam.taskflow.ui.screens.ProjectsScreen
|
|
46
|
+
import uz.rsteam.taskflow.ui.screens.SettingsScreen
|
|
47
|
+
import uz.rsteam.taskflow.ui.screens.TaskDetailScreen
|
|
48
|
+
import uz.rsteam.taskflow.ui.sheets.AssignUserSheet
|
|
49
|
+
import uz.rsteam.taskflow.ui.sheets.NewProjectSheet
|
|
50
|
+
import uz.rsteam.taskflow.ui.sheets.TaskEditorSheet
|
|
51
|
+
import uz.rsteam.taskflow.ui.theme.TaskFlowTheme
|
|
52
|
+
import java.time.LocalDate
|
|
53
|
+
import java.time.LocalDateTime
|
|
54
|
+
import java.util.UUID
|
|
55
|
+
|
|
56
|
+
@Composable
|
|
57
|
+
fun TaskFlowApp() {
|
|
58
|
+
val users = remember { SampleData.users }
|
|
59
|
+
val currentUser = users.first()
|
|
60
|
+
val projects = remember { mutableStateListOf<Project>().apply { addAll(SampleData.projects) } }
|
|
61
|
+
val tasks = remember { mutableStateListOf<Task>().apply { addAll(SampleData.tasks(currentUser)) } }
|
|
62
|
+
|
|
63
|
+
var route by remember { mutableStateOf<Route>(Route.TopLevel(TopLevelScreen.Home)) }
|
|
64
|
+
var sheetState by remember { mutableStateOf<SheetState?>(null) }
|
|
65
|
+
var searchQuery by remember { mutableStateOf("") }
|
|
66
|
+
var activeFilter by remember { mutableStateOf(TaskFilter.Today) }
|
|
67
|
+
|
|
68
|
+
TaskFlowTheme {
|
|
69
|
+
BoxWithConstraints {
|
|
70
|
+
val compact = maxWidth < 600.dp
|
|
71
|
+
val primaryColor = MaterialTheme.colorScheme.primary
|
|
72
|
+
val selectedTopLevel = when (val r = route) {
|
|
73
|
+
is Route.TopLevel -> r.screen
|
|
74
|
+
is Route.ProjectDetail -> TopLevelScreen.Projects
|
|
75
|
+
is Route.ProfileEdit -> TopLevelScreen.Settings
|
|
76
|
+
is Route.TaskDetail -> TopLevelScreen.Home
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Scaffold(
|
|
80
|
+
containerColor = MaterialTheme.colorScheme.background,
|
|
81
|
+
bottomBar = {
|
|
82
|
+
if (compact) {
|
|
83
|
+
NavigationBar {
|
|
84
|
+
TopLevelScreen.entries.forEach { item ->
|
|
85
|
+
NavigationBarItem(
|
|
86
|
+
selected = item == selectedTopLevel,
|
|
87
|
+
onClick = { route = Route.TopLevel(item) },
|
|
88
|
+
icon = {
|
|
89
|
+
Icon(
|
|
90
|
+
when (item) {
|
|
91
|
+
TopLevelScreen.Home -> Icons.Outlined.Home
|
|
92
|
+
TopLevelScreen.Projects -> Icons.Outlined.Folder
|
|
93
|
+
TopLevelScreen.Calendar -> Icons.Outlined.CalendarMonth
|
|
94
|
+
TopLevelScreen.Settings -> Icons.Outlined.Settings
|
|
95
|
+
},
|
|
96
|
+
contentDescription = null
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
label = { Text(textFor(item)) }
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
floatingActionButton = {
|
|
106
|
+
if (selectedTopLevel == TopLevelScreen.Home) {
|
|
107
|
+
FloatingActionButton(onClick = { sheetState = SheetState.CreateTask }) {
|
|
108
|
+
Text("+")
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
) { innerPadding ->
|
|
113
|
+
Row(modifier = Modifier.fillMaxSize()) {
|
|
114
|
+
if (!compact) {
|
|
115
|
+
NavigationRail(modifier = Modifier.fillMaxHeight().navigationBarsPadding()) {
|
|
116
|
+
TopLevelScreen.entries.forEach { item ->
|
|
117
|
+
NavigationRailItem(
|
|
118
|
+
selected = item == selectedTopLevel,
|
|
119
|
+
onClick = { route = Route.TopLevel(item) },
|
|
120
|
+
icon = {
|
|
121
|
+
Icon(
|
|
122
|
+
when (item) {
|
|
123
|
+
TopLevelScreen.Home -> Icons.Outlined.Home
|
|
124
|
+
TopLevelScreen.Projects -> Icons.Outlined.Folder
|
|
125
|
+
TopLevelScreen.Calendar -> Icons.Outlined.CalendarMonth
|
|
126
|
+
TopLevelScreen.Settings -> Icons.Outlined.Settings
|
|
127
|
+
},
|
|
128
|
+
contentDescription = null
|
|
129
|
+
)
|
|
130
|
+
},
|
|
131
|
+
label = { Text(textFor(item)) }
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
when (val currentRoute = route) {
|
|
138
|
+
is Route.TopLevel -> when (currentRoute.screen) {
|
|
139
|
+
TopLevelScreen.Home -> HomeScreen(
|
|
140
|
+
currentUser = currentUser,
|
|
141
|
+
projects = projects,
|
|
142
|
+
tasks = tasks,
|
|
143
|
+
activeFilter = activeFilter,
|
|
144
|
+
searchQuery = searchQuery,
|
|
145
|
+
onSearchChange = { searchQuery = it },
|
|
146
|
+
onFilterChange = { activeFilter = it },
|
|
147
|
+
onSelectTask = { route = Route.TaskDetail(it) },
|
|
148
|
+
onCreateTask = { sheetState = SheetState.CreateTask }
|
|
149
|
+
)
|
|
150
|
+
TopLevelScreen.Projects -> ProjectsScreen(
|
|
151
|
+
projects = projects.map { it.copy(taskCount = tasks.count { t -> t.projectId == it.id }) },
|
|
152
|
+
onOpenProject = { route = Route.ProjectDetail(it) },
|
|
153
|
+
onNewProject = { sheetState = SheetState.NewProject }
|
|
154
|
+
)
|
|
155
|
+
TopLevelScreen.Calendar -> CalendarScreen()
|
|
156
|
+
TopLevelScreen.Settings -> SettingsScreen(
|
|
157
|
+
currentUser = currentUser,
|
|
158
|
+
preferences = uz.rsteam.taskflow.model.Preferences(),
|
|
159
|
+
onPreferencesChange = {},
|
|
160
|
+
onEditProfile = { route = Route.ProfileEdit }
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
is Route.TaskDetail -> {
|
|
164
|
+
val task = tasks.firstOrNull { it.id == currentRoute.taskId }
|
|
165
|
+
if (task != null) {
|
|
166
|
+
TaskDetailScreen(
|
|
167
|
+
task = task,
|
|
168
|
+
project = projects.firstOrNull { it.id == task.projectId },
|
|
169
|
+
onEdit = { sheetState = SheetState.EditTask(task.id) },
|
|
170
|
+
onToggleStatus = {
|
|
171
|
+
val idx = tasks.indexOfFirst { it.id == task.id }
|
|
172
|
+
if (idx >= 0) tasks[idx] = task.copy(
|
|
173
|
+
status = if (task.status == TaskStatus.Done) TaskStatus.Todo else TaskStatus.Done,
|
|
174
|
+
updatedAt = LocalDateTime.now()
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
onDelete = {
|
|
178
|
+
tasks.removeAll { it.id == task.id }
|
|
179
|
+
route = Route.TopLevel(TopLevelScreen.Home)
|
|
180
|
+
},
|
|
181
|
+
onProjectClick = { task.projectId?.let { route = Route.ProjectDetail(it) } },
|
|
182
|
+
onAssign = { sheetState = SheetState.AssignTask(task.id) }
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
is Route.ProjectDetail -> {
|
|
187
|
+
val project = projects.firstOrNull { it.id == currentRoute.projectId }
|
|
188
|
+
if (project != null) {
|
|
189
|
+
ProjectDetailScreen(project = project, tasks = tasks.filter { it.projectId == project.id }, onOpenTask = {
|
|
190
|
+
route = Route.TaskDetail(it)
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
Route.ProfileEdit -> ProfileEditScreen(currentUser = currentUser) { _, _ ->
|
|
195
|
+
route = Route.TopLevel(TopLevelScreen.Settings)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
when (val sheet = sheetState) {
|
|
202
|
+
SheetState.CreateTask -> TaskEditorSheet(
|
|
203
|
+
title = "New task",
|
|
204
|
+
initial = TaskEditorDraft(priority = Priority.Medium),
|
|
205
|
+
onDismiss = { sheetState = null },
|
|
206
|
+
onSubmit = { draft ->
|
|
207
|
+
tasks.add(0, toTask(draft, currentUser))
|
|
208
|
+
sheetState = null
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
is SheetState.EditTask -> {
|
|
212
|
+
val task = tasks.firstOrNull { it.id == sheet.taskId }
|
|
213
|
+
if (task != null) {
|
|
214
|
+
TaskEditorSheet(
|
|
215
|
+
title = "Edit task",
|
|
216
|
+
initial = TaskEditorDraft(
|
|
217
|
+
title = task.title,
|
|
218
|
+
description = task.description ?: "",
|
|
219
|
+
projectId = task.projectId,
|
|
220
|
+
priority = task.priority,
|
|
221
|
+
dueDate = task.dueDate?.toString() ?: "",
|
|
222
|
+
tags = task.tags.joinToString(", "),
|
|
223
|
+
assignToSelf = task.assignee?.id == currentUser.id
|
|
224
|
+
),
|
|
225
|
+
onDismiss = { sheetState = null },
|
|
226
|
+
onSubmit = { draft ->
|
|
227
|
+
val idx = tasks.indexOfFirst { it.id == task.id }
|
|
228
|
+
if (idx >= 0) tasks[idx] = task.copy(
|
|
229
|
+
title = draft.title,
|
|
230
|
+
description = draft.description.ifBlank { null },
|
|
231
|
+
priority = draft.priority,
|
|
232
|
+
dueDate = draft.dueDate.toLocalDateOrNull(),
|
|
233
|
+
projectId = draft.projectId,
|
|
234
|
+
tags = draft.tags.toTags(),
|
|
235
|
+
assignee = if (draft.assignToSelf) currentUser else null,
|
|
236
|
+
updatedAt = LocalDateTime.now()
|
|
237
|
+
)
|
|
238
|
+
sheetState = null
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
SheetState.NewProject -> NewProjectSheet(
|
|
244
|
+
onDismiss = { sheetState = null },
|
|
245
|
+
onCreate = { draft: NewProjectDraft ->
|
|
246
|
+
projects.add(Project(UUID.randomUUID().toString(), draft.name.trim(), primaryColor, Icons.Outlined.Folder, 0))
|
|
247
|
+
sheetState = null
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
is SheetState.AssignTask -> AssignUserSheet(users = users, onDismiss = { sheetState = null }) { user ->
|
|
251
|
+
val idx = tasks.indexOfFirst { it.id == sheet.taskId }
|
|
252
|
+
if (idx >= 0) tasks[idx] = tasks[idx].copy(assignee = user, updatedAt = LocalDateTime.now())
|
|
253
|
+
sheetState = null
|
|
254
|
+
}
|
|
255
|
+
null -> Unit
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private fun textFor(screen: TopLevelScreen) = when (screen) {
|
|
262
|
+
TopLevelScreen.Home -> "Tasks"
|
|
263
|
+
TopLevelScreen.Projects -> "Projects"
|
|
264
|
+
TopLevelScreen.Calendar -> "Calendar"
|
|
265
|
+
TopLevelScreen.Settings -> "Settings"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fun toTask(draft: TaskEditorDraft, currentUser: uz.rsteam.taskflow.model.User): Task = Task(
|
|
269
|
+
id = UUID.randomUUID().toString(),
|
|
270
|
+
title = draft.title.trim(),
|
|
271
|
+
description = draft.description.trim().ifBlank { null },
|
|
272
|
+
status = TaskStatus.Todo,
|
|
273
|
+
priority = draft.priority,
|
|
274
|
+
dueDate = draft.dueDate.toLocalDateOrNull(),
|
|
275
|
+
projectId = draft.projectId,
|
|
276
|
+
tags = draft.tags.toTags(),
|
|
277
|
+
createdAt = LocalDateTime.now(),
|
|
278
|
+
updatedAt = LocalDateTime.now(),
|
|
279
|
+
assignee = if (draft.assignToSelf) currentUser else null
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
private fun String.toLocalDateOrNull(): LocalDate? = runCatching { LocalDate.parse(this) }.getOrNull()
|
|
283
|
+
private fun String.toTags(): List<String> = split(",").map { it.trim() }.filter { it.isNotBlank() }
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
package uz.rsteam.taskflow.model
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.graphics.Color
|
|
4
|
+
import androidx.compose.ui.graphics.vector.ImageVector
|
|
5
|
+
import uz.rsteam.taskflow.R
|
|
6
|
+
import java.time.LocalDate
|
|
7
|
+
import java.time.LocalDateTime
|
|
8
|
+
|
|
9
|
+
enum class TopLevelScreen(val titleRes: Int) {
|
|
10
|
+
Home(R.string.nav_tasks),
|
|
11
|
+
Projects(R.string.nav_projects),
|
|
12
|
+
Calendar(R.string.nav_calendar),
|
|
13
|
+
Settings(R.string.nav_settings)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
sealed interface Route {
|
|
17
|
+
data class TopLevel(val screen: TopLevelScreen) : Route
|
|
18
|
+
data class TaskDetail(val taskId: String) : Route
|
|
19
|
+
data class ProjectDetail(val projectId: String) : Route
|
|
20
|
+
data object ProfileEdit : Route
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
sealed interface SheetState {
|
|
24
|
+
data object CreateTask : SheetState
|
|
25
|
+
data class EditTask(val taskId: String) : SheetState
|
|
26
|
+
data object NewProject : SheetState
|
|
27
|
+
data class AssignTask(val taskId: String) : SheetState
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
enum class TaskFilter(val labelRes: Int) {
|
|
31
|
+
All(R.string.home_filter_all),
|
|
32
|
+
Today(R.string.home_filter_today),
|
|
33
|
+
Upcoming(R.string.home_filter_upcoming),
|
|
34
|
+
Done(R.string.home_filter_done)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
enum class TaskStatus(val labelRes: Int) {
|
|
38
|
+
Todo(R.string.status_todo),
|
|
39
|
+
InProgress(R.string.status_in_progress),
|
|
40
|
+
Done(R.string.status_done)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
enum class Priority(val labelRes: Int, val color: Color) {
|
|
44
|
+
Low(R.string.priority_low, Color(0xFF9CA3AF)),
|
|
45
|
+
Medium(R.string.priority_medium, Color(0xFF3B82D4)),
|
|
46
|
+
High(R.string.priority_high, Color(0xFFD4920E)),
|
|
47
|
+
Urgent(R.string.priority_urgent, Color(0xFFD43B3B))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
data class User(
|
|
51
|
+
val id: String,
|
|
52
|
+
val name: String,
|
|
53
|
+
val firstName: String,
|
|
54
|
+
val email: String
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
data class Project(
|
|
58
|
+
val id: String,
|
|
59
|
+
val name: String,
|
|
60
|
+
val color: Color,
|
|
61
|
+
val icon: ImageVector,
|
|
62
|
+
val taskCount: Int
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
data class MediaAttachment(
|
|
66
|
+
val source: String,
|
|
67
|
+
val mediaType: String,
|
|
68
|
+
val title: String
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
data class Task(
|
|
72
|
+
val id: String,
|
|
73
|
+
val title: String,
|
|
74
|
+
val description: String?,
|
|
75
|
+
val status: TaskStatus,
|
|
76
|
+
val priority: Priority,
|
|
77
|
+
val dueDate: LocalDate?,
|
|
78
|
+
val projectId: String?,
|
|
79
|
+
val tags: List<String>,
|
|
80
|
+
val createdAt: LocalDateTime,
|
|
81
|
+
val updatedAt: LocalDateTime,
|
|
82
|
+
val assignee: User?,
|
|
83
|
+
val attachment: MediaAttachment? = null
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
data class Preferences(
|
|
87
|
+
val defaultPriority: Priority = Priority.Medium,
|
|
88
|
+
val notificationsEnabled: Boolean = true,
|
|
89
|
+
val remindersEnabled: Boolean = true
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
data class TaskEditorDraft(
|
|
93
|
+
val title: String = "",
|
|
94
|
+
val description: String = "",
|
|
95
|
+
val projectId: String? = null,
|
|
96
|
+
val priority: Priority = Priority.Medium,
|
|
97
|
+
val dueDate: String = "",
|
|
98
|
+
val tags: String = "",
|
|
99
|
+
val assignToSelf: Boolean = true
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
data class NewProjectDraft(
|
|
103
|
+
val name: String = "",
|
|
104
|
+
val colorName: String = "Indigo",
|
|
105
|
+
val iconName: String = "Folder"
|
|
106
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package uz.rsteam.taskflow.model
|
|
2
|
+
|
|
3
|
+
import androidx.compose.material.icons.Icons
|
|
4
|
+
import androidx.compose.material.icons.outlined.CheckCircle
|
|
5
|
+
import androidx.compose.material.icons.outlined.Folder
|
|
6
|
+
import androidx.compose.material.icons.outlined.Star
|
|
7
|
+
import androidx.compose.ui.graphics.Color
|
|
8
|
+
import java.time.LocalDate
|
|
9
|
+
import java.time.LocalDateTime
|
|
10
|
+
|
|
11
|
+
object SampleData {
|
|
12
|
+
val users = listOf(
|
|
13
|
+
User("u1", "Amina Patel", "Amina", "amina@taskflow.app"),
|
|
14
|
+
User("u2", "Jonas Reed", "Jonas", "jonas@taskflow.app"),
|
|
15
|
+
User("u3", "Mika Chen", "Mika", "mika@taskflow.app")
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
val projects = listOf(
|
|
19
|
+
Project("p1", "Product Launch", Color(0xFF5B4FE8), Icons.Outlined.Star, 3),
|
|
20
|
+
Project("p2", "Website Refresh", Color(0xFF3B82D4), Icons.Outlined.Folder, 2),
|
|
21
|
+
Project("p3", "Sprint Rituals", Color(0xFF2D9D5E), Icons.Outlined.CheckCircle, 2)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
fun tasks(currentUser: User): List<Task> = listOf(
|
|
25
|
+
Task(
|
|
26
|
+
id = "t1",
|
|
27
|
+
title = "Finalize launch checklist",
|
|
28
|
+
description = "Confirm release notes, QA sign-off, and staged rollout timing.",
|
|
29
|
+
status = TaskStatus.InProgress,
|
|
30
|
+
priority = Priority.High,
|
|
31
|
+
dueDate = LocalDate.now(),
|
|
32
|
+
projectId = "p1",
|
|
33
|
+
tags = listOf("launch", "ops"),
|
|
34
|
+
createdAt = LocalDateTime.now().minusDays(4),
|
|
35
|
+
updatedAt = LocalDateTime.now().minusHours(2),
|
|
36
|
+
assignee = currentUser,
|
|
37
|
+
attachment = MediaAttachment(
|
|
38
|
+
source = "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
|
39
|
+
mediaType = "video",
|
|
40
|
+
title = "Launch teaser draft"
|
|
41
|
+
)
|
|
42
|
+
),
|
|
43
|
+
Task(
|
|
44
|
+
id = "t2",
|
|
45
|
+
title = "Draft homepage copy",
|
|
46
|
+
description = "Need a stronger value proposition and one proof block for social trust.",
|
|
47
|
+
status = TaskStatus.Todo,
|
|
48
|
+
priority = Priority.Medium,
|
|
49
|
+
dueDate = LocalDate.now().plusDays(2),
|
|
50
|
+
projectId = "p2",
|
|
51
|
+
tags = listOf("copy", "marketing"),
|
|
52
|
+
createdAt = LocalDateTime.now().minusDays(2),
|
|
53
|
+
updatedAt = LocalDateTime.now().minusHours(10),
|
|
54
|
+
assignee = users[1]
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
package uz.rsteam.taskflow.ui.components
|
|
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.Row
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
9
|
+
import androidx.compose.foundation.layout.padding
|
|
10
|
+
import androidx.compose.foundation.layout.size
|
|
11
|
+
import androidx.compose.foundation.shape.CircleShape
|
|
12
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
13
|
+
import androidx.compose.material.icons.Icons
|
|
14
|
+
import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight
|
|
15
|
+
import androidx.compose.material.icons.outlined.CheckCircle
|
|
16
|
+
import androidx.compose.material3.Card
|
|
17
|
+
import androidx.compose.material3.ElevatedCard
|
|
18
|
+
import androidx.compose.material3.Icon
|
|
19
|
+
import androidx.compose.material3.MaterialTheme
|
|
20
|
+
import androidx.compose.material3.Text
|
|
21
|
+
import androidx.compose.material3.TextButton
|
|
22
|
+
import androidx.compose.runtime.Composable
|
|
23
|
+
import androidx.compose.ui.Alignment
|
|
24
|
+
import androidx.compose.ui.Modifier
|
|
25
|
+
import androidx.compose.ui.graphics.Color
|
|
26
|
+
import androidx.compose.ui.graphics.vector.ImageVector
|
|
27
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
28
|
+
import androidx.compose.ui.unit.dp
|
|
29
|
+
|
|
30
|
+
@Composable
|
|
31
|
+
fun EmptyStateCard(title: String, body: String) {
|
|
32
|
+
ElevatedCard {
|
|
33
|
+
Column(
|
|
34
|
+
modifier = Modifier
|
|
35
|
+
.fillMaxWidth()
|
|
36
|
+
.padding(24.dp),
|
|
37
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
38
|
+
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
39
|
+
) {
|
|
40
|
+
Icon(Icons.Outlined.CheckCircle, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(36.dp))
|
|
41
|
+
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
|
42
|
+
Text(body, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Composable
|
|
48
|
+
fun SectionCard(title: String, content: @Composable () -> Unit) {
|
|
49
|
+
ElevatedCard {
|
|
50
|
+
Column(
|
|
51
|
+
modifier = Modifier
|
|
52
|
+
.fillMaxWidth()
|
|
53
|
+
.padding(16.dp),
|
|
54
|
+
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
55
|
+
) {
|
|
56
|
+
Text(title, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
57
|
+
content()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Composable
|
|
63
|
+
fun StatCard(label: String, value: String) {
|
|
64
|
+
Card {
|
|
65
|
+
Column(modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
66
|
+
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
67
|
+
Text(value, fontWeight = FontWeight.SemiBold)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Composable
|
|
73
|
+
fun DetailRow(icon: ImageVector, label: String, value: String, onClick: (() -> Unit)? = null) {
|
|
74
|
+
val content: @Composable () -> Unit = {
|
|
75
|
+
Row(
|
|
76
|
+
modifier = Modifier
|
|
77
|
+
.fillMaxWidth()
|
|
78
|
+
.padding(vertical = 8.dp),
|
|
79
|
+
verticalAlignment = Alignment.CenterVertically,
|
|
80
|
+
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
81
|
+
) {
|
|
82
|
+
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
83
|
+
Column(modifier = Modifier.weight(1f)) {
|
|
84
|
+
Text(label, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
85
|
+
Text(value, fontWeight = FontWeight.Medium)
|
|
86
|
+
}
|
|
87
|
+
if (onClick != null) Icon(Icons.AutoMirrored.Outlined.KeyboardArrowRight, contentDescription = null)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (onClick != null) TextButton(onClick = onClick, modifier = Modifier.fillMaxWidth()) { content() } else content()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Composable
|
|
94
|
+
fun Avatar(name: String) {
|
|
95
|
+
val initials = name.split(" ").take(2).joinToString("") { it.firstOrNull()?.uppercase() ?: "" }
|
|
96
|
+
Box(
|
|
97
|
+
modifier = Modifier
|
|
98
|
+
.size(40.dp)
|
|
99
|
+
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f), CircleShape),
|
|
100
|
+
contentAlignment = Alignment.Center
|
|
101
|
+
) {
|
|
102
|
+
Text(initials, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.SemiBold)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Composable
|
|
107
|
+
fun PriorityDot(color: Color) {
|
|
108
|
+
Box(modifier = Modifier.size(12.dp).background(color, RoundedCornerShape(999.dp)))
|
|
109
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package uz.rsteam.taskflow.ui.screens
|
|
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.Row
|
|
7
|
+
import androidx.compose.foundation.layout.Spacer
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
9
|
+
import androidx.compose.foundation.layout.height
|
|
10
|
+
import androidx.compose.foundation.layout.width
|
|
11
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
12
|
+
import androidx.compose.foundation.lazy.items
|
|
13
|
+
import androidx.compose.material.icons.Icons
|
|
14
|
+
import androidx.compose.material.icons.outlined.Add
|
|
15
|
+
import androidx.compose.material.icons.outlined.Search
|
|
16
|
+
import androidx.compose.material3.Button
|
|
17
|
+
import androidx.compose.material3.ElevatedCard
|
|
18
|
+
import androidx.compose.material3.FilterChip
|
|
19
|
+
import androidx.compose.material3.Icon
|
|
20
|
+
import androidx.compose.material3.MaterialTheme
|
|
21
|
+
import androidx.compose.material3.OutlinedTextField
|
|
22
|
+
import androidx.compose.material3.Text
|
|
23
|
+
import androidx.compose.runtime.Composable
|
|
24
|
+
import androidx.compose.ui.Alignment
|
|
25
|
+
import androidx.compose.ui.Modifier
|
|
26
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
27
|
+
import androidx.compose.ui.unit.dp
|
|
28
|
+
import uz.rsteam.taskflow.model.Project
|
|
29
|
+
import uz.rsteam.taskflow.model.Task
|
|
30
|
+
import uz.rsteam.taskflow.model.TaskFilter
|
|
31
|
+
import uz.rsteam.taskflow.model.TaskStatus
|
|
32
|
+
import uz.rsteam.taskflow.model.User
|
|
33
|
+
import uz.rsteam.taskflow.ui.components.EmptyStateCard
|
|
34
|
+
import uz.rsteam.taskflow.ui.components.PriorityDot
|
|
35
|
+
import java.time.LocalDate
|
|
36
|
+
|
|
37
|
+
@Composable
|
|
38
|
+
fun HomeScreen(
|
|
39
|
+
currentUser: User,
|
|
40
|
+
projects: List<Project>,
|
|
41
|
+
tasks: List<Task>,
|
|
42
|
+
activeFilter: TaskFilter,
|
|
43
|
+
searchQuery: String,
|
|
44
|
+
onSearchChange: (String) -> Unit,
|
|
45
|
+
onFilterChange: (TaskFilter) -> Unit,
|
|
46
|
+
onSelectTask: (String) -> Unit,
|
|
47
|
+
onCreateTask: () -> Unit
|
|
48
|
+
) {
|
|
49
|
+
val filteredTasks = tasks.filter { task ->
|
|
50
|
+
val filterMatch = when (activeFilter) {
|
|
51
|
+
TaskFilter.All -> true
|
|
52
|
+
TaskFilter.Today -> task.dueDate == LocalDate.now()
|
|
53
|
+
TaskFilter.Upcoming -> task.dueDate?.isAfter(LocalDate.now()) == true
|
|
54
|
+
TaskFilter.Done -> task.status == TaskStatus.Done
|
|
55
|
+
}
|
|
56
|
+
val haystack = (task.title + " " + (task.description ?: "") + " " + task.tags.joinToString(" ") + " " + (projects.firstOrNull { it.id == task.projectId }?.name ?: ""))
|
|
57
|
+
filterMatch && haystack.contains(searchQuery, ignoreCase = true)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
LazyColumn(contentPadding = PaddingValues(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
61
|
+
item {
|
|
62
|
+
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
63
|
+
Text("Good day, ${currentUser.firstName}", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
|
64
|
+
Text("${filteredTasks.size} tasks visible", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
item {
|
|
68
|
+
OutlinedTextField(
|
|
69
|
+
value = searchQuery,
|
|
70
|
+
onValueChange = onSearchChange,
|
|
71
|
+
modifier = Modifier.fillMaxWidth(),
|
|
72
|
+
leadingIcon = { Icon(Icons.Outlined.Search, contentDescription = null) },
|
|
73
|
+
placeholder = { Text("Search tasks") }
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
item {
|
|
77
|
+
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
78
|
+
TaskFilter.entries.forEach { filter ->
|
|
79
|
+
FilterChip(selected = filter == activeFilter, onClick = { onFilterChange(filter) }, label = { Text(filter.name) })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
item {
|
|
84
|
+
Button(onClick = onCreateTask) {
|
|
85
|
+
Icon(Icons.Outlined.Add, contentDescription = null)
|
|
86
|
+
Spacer(Modifier.width(8.dp))
|
|
87
|
+
Text("New task")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (filteredTasks.isEmpty()) {
|
|
92
|
+
item { EmptyStateCard(title = "All caught up!", body = "No tasks match this filter") }
|
|
93
|
+
} else {
|
|
94
|
+
items(filteredTasks, key = { it.id }) { task ->
|
|
95
|
+
ElevatedCard(onClick = { onSelectTask(task.id) }) {
|
|
96
|
+
Row(
|
|
97
|
+
modifier = Modifier.fillMaxWidth(),
|
|
98
|
+
horizontalArrangement = Arrangement.SpaceBetween,
|
|
99
|
+
verticalAlignment = Alignment.CenterVertically
|
|
100
|
+
) {
|
|
101
|
+
Column(modifier = Modifier.weight(1f)) {
|
|
102
|
+
Text(task.title, fontWeight = FontWeight.SemiBold)
|
|
103
|
+
Text(task.tags.joinToString(", "), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
104
|
+
}
|
|
105
|
+
PriorityDot(task.priority.color)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
item { Spacer(Modifier.height(80.dp)) }
|
|
111
|
+
}
|
|
112
|
+
}
|