openuispec 0.1.45 → 0.1.47

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 (138) hide show
  1. package/README.md +17 -5
  2. package/cli/init.ts +21 -3
  3. package/examples/social-app/.mcp.json +10 -0
  4. package/examples/social-app/AGENTS.md +114 -0
  5. package/examples/social-app/CLAUDE.md +114 -0
  6. package/examples/social-app/README.md +19 -0
  7. package/examples/social-app/backend/.gitkeep +1 -0
  8. package/examples/social-app/generated/android/social-app/app/build.gradle.kts +92 -0
  9. package/examples/social-app/generated/android/social-app/app/src/main/AndroidManifest.xml +26 -0
  10. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/AppContainer.kt +20 -0
  11. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/MainActivity.kt +35 -0
  12. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/SocialAppApplication.kt +13 -0
  13. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/MockData.kt +98 -0
  14. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/AppPreferences.kt +19 -0
  15. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/DataStorePreferencesRepository.kt +68 -0
  16. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/data/preferences/PreferencesRepository.kt +15 -0
  17. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/model/Models.kt +34 -0
  18. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/MainShell.kt +390 -0
  19. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/Components.kt +234 -0
  20. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/components/ContractPrimitives.kt +641 -0
  21. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/navigation/RootComponent.kt +113 -0
  22. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ChatDetailScreen.kt +212 -0
  23. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/CreatePostScreen.kt +113 -0
  24. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/DiscoverScreen.kt +137 -0
  25. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/EditProfileScreen.kt +180 -0
  26. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/HomeFeedScreen.kt +157 -0
  27. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/MessagesInboxScreen.kt +85 -0
  28. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/NotificationsScreen.kt +74 -0
  29. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/PostDetailScreen.kt +293 -0
  30. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/ProfileSelfScreen.kt +116 -0
  31. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SearchResultsScreen.kt +161 -0
  32. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsScreen.kt +164 -0
  33. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/SettingsStore.kt +95 -0
  34. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/screens/UserProfileScreen.kt +123 -0
  35. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Color.kt +33 -0
  36. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Shape.kt +41 -0
  37. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Spacing.kt +20 -0
  38. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Theme.kt +82 -0
  39. package/examples/social-app/generated/android/social-app/app/src/main/java/com/social/app/ui/theme/Type.kt +60 -0
  40. package/examples/social-app/generated/android/social-app/app/src/main/res/drawable/ic_launcher_foreground.xml +9 -0
  41. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  42. package/examples/social-app/generated/android/social-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  43. package/examples/social-app/generated/android/social-app/app/src/main/res/values/strings.xml +91 -0
  44. package/examples/social-app/generated/android/social-app/app/src/main/res/values/themes.xml +10 -0
  45. package/examples/social-app/generated/android/social-app/app/src/main/res/values-ru/strings.xml +79 -0
  46. package/examples/social-app/generated/android/social-app/app/src/main/res/values-uz/strings.xml +79 -0
  47. package/examples/social-app/generated/android/social-app/app/src/main/xml/AndroidManifest.xml +23 -0
  48. package/examples/social-app/generated/android/social-app/build.gradle.kts +6 -0
  49. package/examples/social-app/generated/android/social-app/gradle/libs.versions.toml +48 -0
  50. package/examples/social-app/generated/android/social-app/gradle/wrapper/gradle-wrapper.properties +8 -0
  51. package/examples/social-app/generated/android/social-app/gradle.properties +11 -0
  52. package/examples/social-app/generated/android/social-app/gradlew +25 -0
  53. package/examples/social-app/generated/android/social-app/settings.gradle.kts +23 -0
  54. package/examples/social-app/generated/web/social-app/index.html +12 -0
  55. package/examples/social-app/generated/web/social-app/package-lock.json +2517 -0
  56. package/examples/social-app/generated/web/social-app/package.json +27 -0
  57. package/examples/social-app/generated/web/social-app/src/app/App.tsx +58 -0
  58. package/examples/social-app/generated/web/social-app/src/components/Shell.tsx +259 -0
  59. package/examples/social-app/generated/web/social-app/src/components/cards.tsx +317 -0
  60. package/examples/social-app/generated/web/social-app/src/components/ui.tsx +340 -0
  61. package/examples/social-app/generated/web/social-app/src/flows/CreatePostFlow.tsx +86 -0
  62. package/examples/social-app/generated/web/social-app/src/i18n.tsx +59 -0
  63. package/examples/social-app/generated/web/social-app/src/lib/icons.tsx +85 -0
  64. package/examples/social-app/generated/web/social-app/src/lib/tokens.ts +70 -0
  65. package/examples/social-app/generated/web/social-app/src/lib/utils.ts +97 -0
  66. package/examples/social-app/generated/web/social-app/src/locales/en.json +67 -0
  67. package/examples/social-app/generated/web/social-app/src/locales/ru.json +67 -0
  68. package/examples/social-app/generated/web/social-app/src/locales/uz.json +67 -0
  69. package/examples/social-app/generated/web/social-app/src/main.tsx +16 -0
  70. package/examples/social-app/generated/web/social-app/src/screens/ChatDetailScreen.tsx +90 -0
  71. package/examples/social-app/generated/web/social-app/src/screens/DiscoverScreen.tsx +86 -0
  72. package/examples/social-app/generated/web/social-app/src/screens/EditProfileScreen.tsx +57 -0
  73. package/examples/social-app/generated/web/social-app/src/screens/HomeFeedScreen.tsx +103 -0
  74. package/examples/social-app/generated/web/social-app/src/screens/MessagesInboxScreen.tsx +52 -0
  75. package/examples/social-app/generated/web/social-app/src/screens/NotificationsScreen.tsx +41 -0
  76. package/examples/social-app/generated/web/social-app/src/screens/PostDetailScreen.tsx +115 -0
  77. package/examples/social-app/generated/web/social-app/src/screens/ProfileSelfScreen.tsx +57 -0
  78. package/examples/social-app/generated/web/social-app/src/screens/ProfileUserScreen.tsx +76 -0
  79. package/examples/social-app/generated/web/social-app/src/screens/SearchResultsScreen.tsx +96 -0
  80. package/examples/social-app/generated/web/social-app/src/screens/SettingsScreen.tsx +79 -0
  81. package/examples/social-app/generated/web/social-app/src/state/store.ts +592 -0
  82. package/examples/social-app/generated/web/social-app/src/styles.css +124 -0
  83. package/examples/social-app/generated/web/social-app/src/vite-env.d.ts +1 -0
  84. package/examples/social-app/generated/web/social-app/tsconfig.json +22 -0
  85. package/examples/social-app/generated/web/social-app/tsconfig.node.json +13 -0
  86. package/examples/social-app/generated/web/social-app/tsconfig.node.tsbuildinfo +1 -0
  87. package/examples/social-app/generated/web/social-app/tsconfig.tsbuildinfo +1 -0
  88. package/examples/social-app/generated/web/social-app/vite.config.d.ts +2 -0
  89. package/examples/social-app/generated/web/social-app/vite.config.js +6 -0
  90. package/examples/social-app/generated/web/social-app/vite.config.ts +7 -0
  91. package/examples/social-app/openuispec/README.md +56 -0
  92. package/examples/social-app/openuispec/contracts/.gitkeep +0 -0
  93. package/examples/social-app/openuispec/contracts/action_trigger.yaml +104 -0
  94. package/examples/social-app/openuispec/contracts/collection.yaml +43 -0
  95. package/examples/social-app/openuispec/contracts/data_display.yaml +47 -0
  96. package/examples/social-app/openuispec/contracts/feedback.yaml +49 -0
  97. package/examples/social-app/openuispec/contracts/input_field.yaml +41 -0
  98. package/examples/social-app/openuispec/contracts/nav_container.yaml +34 -0
  99. package/examples/social-app/openuispec/contracts/surface.yaml +41 -0
  100. package/examples/social-app/openuispec/flows/.gitkeep +0 -0
  101. package/examples/social-app/openuispec/flows/create_post.yaml +66 -0
  102. package/examples/social-app/openuispec/locales/.gitkeep +0 -0
  103. package/examples/social-app/openuispec/locales/en.json +67 -0
  104. package/examples/social-app/openuispec/locales/ru.json +67 -0
  105. package/examples/social-app/openuispec/locales/uz.json +67 -0
  106. package/examples/social-app/openuispec/openuispec.yaml +214 -0
  107. package/examples/social-app/openuispec/platform/.gitkeep +0 -0
  108. package/examples/social-app/openuispec/platform/android.yaml +30 -0
  109. package/examples/social-app/openuispec/platform/ios.yaml +19 -0
  110. package/examples/social-app/openuispec/platform/web.yaml +23 -0
  111. package/examples/social-app/openuispec/screens/.gitkeep +0 -0
  112. package/examples/social-app/openuispec/screens/chat_detail.yaml +53 -0
  113. package/examples/social-app/openuispec/screens/discover.yaml +78 -0
  114. package/examples/social-app/openuispec/screens/edit_profile.yaml +78 -0
  115. package/examples/social-app/openuispec/screens/home_feed.yaml +138 -0
  116. package/examples/social-app/openuispec/screens/messages_inbox.yaml +43 -0
  117. package/examples/social-app/openuispec/screens/notifications.yaml +29 -0
  118. package/examples/social-app/openuispec/screens/post_detail.yaml +86 -0
  119. package/examples/social-app/openuispec/screens/profile_self.yaml +53 -0
  120. package/examples/social-app/openuispec/screens/profile_user.yaml +60 -0
  121. package/examples/social-app/openuispec/screens/search_results.yaml +62 -0
  122. package/examples/social-app/openuispec/screens/settings.yaml +99 -0
  123. package/examples/social-app/openuispec/tokens/.gitkeep +0 -0
  124. package/examples/social-app/openuispec/tokens/color.yaml +76 -0
  125. package/examples/social-app/openuispec/tokens/elevation.yaml +31 -0
  126. package/examples/social-app/openuispec/tokens/icons.yaml +147 -0
  127. package/examples/social-app/openuispec/tokens/layout.yaml +37 -0
  128. package/examples/social-app/openuispec/tokens/motion.yaml +28 -0
  129. package/examples/social-app/openuispec/tokens/spacing.yaml +19 -0
  130. package/examples/social-app/openuispec/tokens/themes.yaml +31 -0
  131. package/examples/social-app/openuispec/tokens/typography.yaml +50 -0
  132. package/examples/social-app/package.json +12 -0
  133. package/examples/taskflow/openuispec/openuispec.yaml +2 -0
  134. package/examples/todo-orbit/openuispec/openuispec.yaml +2 -0
  135. package/mcp-server/index.ts +200 -10
  136. package/package.json +1 -1
  137. package/schema/openuispec.schema.json +7 -0
  138. package/spec/openuispec-v0.1.md +13 -0
@@ -0,0 +1,641 @@
1
+ package com.social.app.ui.components
2
+
3
+ import androidx.compose.foundation.BorderStroke
4
+ import androidx.compose.foundation.clickable
5
+ import androidx.compose.foundation.layout.Arrangement
6
+ import androidx.compose.foundation.layout.Box
7
+ import androidx.compose.foundation.layout.Column
8
+ import androidx.compose.foundation.layout.PaddingValues
9
+ import androidx.compose.foundation.layout.Row
10
+ import androidx.compose.foundation.layout.fillMaxWidth
11
+ import androidx.compose.foundation.layout.heightIn
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.lazy.LazyRow
16
+ import androidx.compose.foundation.lazy.items
17
+ import androidx.compose.foundation.interaction.MutableInteractionSource
18
+ import androidx.compose.foundation.interaction.collectIsPressedAsState
19
+ import androidx.compose.material.icons.Icons
20
+ import androidx.compose.material.icons.filled.Person
21
+ import androidx.compose.material3.AlertDialog
22
+ import androidx.compose.material3.Button
23
+ import androidx.compose.material3.ButtonDefaults
24
+ import androidx.compose.material3.DropdownMenuItem
25
+ import androidx.compose.material3.ExperimentalMaterial3Api
26
+ import androidx.compose.material3.ExposedDropdownMenuBox
27
+ import androidx.compose.material3.ExposedDropdownMenuDefaults
28
+ import androidx.compose.material3.FilterChip
29
+ import androidx.compose.material3.FilterChipDefaults
30
+ import androidx.compose.material3.HorizontalDivider
31
+ import androidx.compose.material3.Icon
32
+ import androidx.compose.material3.MaterialTheme
33
+ import androidx.compose.material3.OutlinedButton
34
+ import androidx.compose.material3.OutlinedTextField
35
+ import androidx.compose.material3.OutlinedTextFieldDefaults
36
+ import androidx.compose.material3.Snackbar
37
+ import androidx.compose.material3.SnackbarHost
38
+ import androidx.compose.material3.SnackbarHostState
39
+ import androidx.compose.material3.Surface
40
+ import androidx.compose.material3.Switch
41
+ import androidx.compose.material3.Text
42
+ import androidx.compose.runtime.getValue
43
+ import androidx.compose.runtime.Composable
44
+ import androidx.compose.animation.core.animateFloatAsState
45
+ import androidx.compose.animation.core.spring
46
+ import androidx.compose.ui.Alignment
47
+ import androidx.compose.ui.Modifier
48
+ import androidx.compose.ui.draw.clip
49
+ import androidx.compose.ui.graphics.Color
50
+ import androidx.compose.ui.graphics.graphicsLayer
51
+ import androidx.compose.ui.layout.ContentScale
52
+ import androidx.compose.foundation.text.KeyboardOptions
53
+ import androidx.compose.ui.text.font.FontWeight
54
+ import androidx.compose.ui.unit.dp
55
+ import coil3.compose.AsyncImage
56
+ import com.social.app.model.User
57
+ import com.social.app.ui.theme.Shapes
58
+ import com.social.app.ui.theme.Spacing
59
+ import com.social.app.ui.theme.TextSecondary
60
+ import com.social.app.ui.theme.TextTertiary
61
+
62
+ enum class ActionTriggerVariant {
63
+ Primary,
64
+ Secondary,
65
+ Destructive,
66
+ }
67
+
68
+ data class ChipOption(
69
+ val value: String,
70
+ val label: String,
71
+ )
72
+
73
+ @Composable
74
+ fun ActionTriggerButton(
75
+ text: String,
76
+ onClick: () -> Unit,
77
+ modifier: Modifier = Modifier,
78
+ variant: ActionTriggerVariant = ActionTriggerVariant.Primary,
79
+ enabled: Boolean = true,
80
+ fullWidth: Boolean = false,
81
+ icon: (@Composable () -> Unit)? = null,
82
+ ) {
83
+ val interactionSource = MutableInteractionSource()
84
+ val isPressed by interactionSource.collectIsPressedAsState()
85
+ val scale by animateFloatAsState(
86
+ targetValue = if (isPressed) 0.97f else 1f,
87
+ animationSpec = spring(dampingRatio = 0.55f, stiffness = 500f),
88
+ label = "actionTriggerScale",
89
+ )
90
+ val shape =
91
+ if (variant == ActionTriggerVariant.Secondary) {
92
+ Shapes.RoundedCapSecondary
93
+ } else {
94
+ Shapes.RoundedCapPrimary
95
+ }
96
+
97
+ val buttonModifier =
98
+ modifier.then(
99
+ if (fullWidth) Modifier.fillMaxWidth() else Modifier,
100
+ ).graphicsLayer {
101
+ scaleX = scale
102
+ scaleY = scale
103
+ }
104
+
105
+ when (variant) {
106
+ ActionTriggerVariant.Primary ->
107
+ Button(
108
+ onClick = onClick,
109
+ enabled = enabled,
110
+ interactionSource = interactionSource,
111
+ modifier = buttonModifier.heightIn(min = 48.dp),
112
+ shape = shape,
113
+ colors =
114
+ ButtonDefaults.buttonColors(
115
+ containerColor = MaterialTheme.colorScheme.primary,
116
+ contentColor = MaterialTheme.colorScheme.onPrimary,
117
+ ),
118
+ contentPadding = PaddingValues(horizontal = 18.dp, vertical = 12.dp),
119
+ ) {
120
+ ActionButtonContent(text = text, icon = icon)
121
+ }
122
+
123
+ ActionTriggerVariant.Secondary ->
124
+ OutlinedButton(
125
+ onClick = onClick,
126
+ enabled = enabled,
127
+ interactionSource = interactionSource,
128
+ modifier = buttonModifier.heightIn(min = 48.dp),
129
+ shape = shape,
130
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
131
+ colors =
132
+ ButtonDefaults.outlinedButtonColors(
133
+ contentColor = MaterialTheme.colorScheme.onSurface,
134
+ ),
135
+ contentPadding = PaddingValues(horizontal = 18.dp, vertical = 12.dp),
136
+ ) {
137
+ ActionButtonContent(text = text, icon = icon)
138
+ }
139
+
140
+ ActionTriggerVariant.Destructive ->
141
+ Button(
142
+ onClick = onClick,
143
+ enabled = enabled,
144
+ interactionSource = interactionSource,
145
+ modifier = buttonModifier.heightIn(min = 48.dp),
146
+ shape = shape,
147
+ colors =
148
+ ButtonDefaults.buttonColors(
149
+ containerColor = MaterialTheme.colorScheme.error,
150
+ contentColor = MaterialTheme.colorScheme.onError,
151
+ ),
152
+ contentPadding = PaddingValues(horizontal = 18.dp, vertical = 12.dp),
153
+ ) {
154
+ ActionButtonContent(text = text, icon = icon)
155
+ }
156
+ }
157
+ }
158
+
159
+ @Composable
160
+ private fun ActionButtonContent(
161
+ text: String,
162
+ icon: (@Composable () -> Unit)?,
163
+ ) {
164
+ Row(
165
+ verticalAlignment = Alignment.CenterVertically,
166
+ horizontalArrangement = Arrangement.spacedBy(Spacing.SM),
167
+ ) {
168
+ icon?.invoke()
169
+ Text(
170
+ text = text,
171
+ style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
172
+ )
173
+ }
174
+ }
175
+
176
+ @OptIn(ExperimentalMaterial3Api::class)
177
+ @Composable
178
+ fun ContractTextField(
179
+ value: String,
180
+ onValueChange: (String) -> Unit,
181
+ label: String,
182
+ modifier: Modifier = Modifier,
183
+ placeholder: String = label,
184
+ singleLine: Boolean = true,
185
+ maxLines: Int = if (singleLine) 1 else 5,
186
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
187
+ leadingIcon: (@Composable () -> Unit)? = null,
188
+ trailingAction: (@Composable () -> Unit)? = null,
189
+ ) {
190
+ OutlinedTextField(
191
+ value = value,
192
+ onValueChange = onValueChange,
193
+ modifier = modifier.fillMaxWidth(),
194
+ label = { Text(label, color = TextSecondary) },
195
+ placeholder = { Text(placeholder, color = TextTertiary) },
196
+ leadingIcon = leadingIcon,
197
+ trailingIcon = trailingAction,
198
+ singleLine = singleLine,
199
+ maxLines = maxLines,
200
+ keyboardOptions = keyboardOptions,
201
+ shape = Shapes.RoundedCapPrimary,
202
+ colors =
203
+ OutlinedTextFieldDefaults.colors(
204
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface,
205
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
206
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
207
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline,
208
+ focusedLabelColor = MaterialTheme.colorScheme.primary,
209
+ cursorColor = MaterialTheme.colorScheme.primary,
210
+ focusedPlaceholderColor = TextTertiary,
211
+ unfocusedPlaceholderColor = TextTertiary,
212
+ ),
213
+ )
214
+ }
215
+
216
+ @OptIn(ExperimentalMaterial3Api::class)
217
+ @Composable
218
+ fun ContractSelectField(
219
+ label: String,
220
+ selectedLabel: String,
221
+ expanded: Boolean,
222
+ onExpandedChange: (Boolean) -> Unit,
223
+ modifier: Modifier = Modifier,
224
+ options: List<ChipOption>,
225
+ onOptionSelected: (ChipOption) -> Unit,
226
+ ) {
227
+ ExposedDropdownMenuBox(
228
+ expanded = expanded,
229
+ onExpandedChange = onExpandedChange,
230
+ modifier = modifier,
231
+ ) {
232
+ OutlinedTextField(
233
+ value = selectedLabel,
234
+ onValueChange = {},
235
+ readOnly = true,
236
+ modifier =
237
+ Modifier
238
+ .menuAnchor(type = androidx.compose.material3.ExposedDropdownMenuAnchorType.PrimaryNotEditable)
239
+ .fillMaxWidth(),
240
+ label = { Text(label, color = TextSecondary) },
241
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
242
+ shape = Shapes.RoundedCapPrimary,
243
+ colors =
244
+ OutlinedTextFieldDefaults.colors(
245
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface,
246
+ focusedContainerColor = MaterialTheme.colorScheme.surface,
247
+ focusedBorderColor = MaterialTheme.colorScheme.primary,
248
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline,
249
+ ),
250
+ )
251
+
252
+ ExposedDropdownMenu(
253
+ expanded = expanded,
254
+ onDismissRequest = { onExpandedChange(false) },
255
+ ) {
256
+ options.forEach { option ->
257
+ DropdownMenuItem(
258
+ text = { Text(option.label) },
259
+ onClick = {
260
+ onOptionSelected(option)
261
+ onExpandedChange(false)
262
+ },
263
+ )
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ @Composable
270
+ fun ContractToggleField(
271
+ label: String,
272
+ checked: Boolean,
273
+ onCheckedChange: (Boolean) -> Unit,
274
+ modifier: Modifier = Modifier,
275
+ ) {
276
+ Row(
277
+ modifier = modifier.fillMaxWidth(),
278
+ horizontalArrangement = Arrangement.SpaceBetween,
279
+ verticalAlignment = Alignment.CenterVertically,
280
+ ) {
281
+ Text(
282
+ text = label,
283
+ style = MaterialTheme.typography.bodyLarge,
284
+ color = MaterialTheme.colorScheme.onSurface,
285
+ )
286
+ Switch(
287
+ checked = checked,
288
+ onCheckedChange = onCheckedChange,
289
+ )
290
+ }
291
+ }
292
+
293
+ @Composable
294
+ fun ContractChipRow(
295
+ options: List<ChipOption>,
296
+ selectedValue: String,
297
+ onSelect: (ChipOption) -> Unit,
298
+ modifier: Modifier = Modifier,
299
+ ) {
300
+ LazyRow(
301
+ modifier = modifier,
302
+ contentPadding = PaddingValues(horizontal = Spacing.MD),
303
+ horizontalArrangement = Arrangement.spacedBy(Spacing.XS),
304
+ ) {
305
+ items(options) { option ->
306
+ FilterChip(
307
+ selected = option.value == selectedValue,
308
+ onClick = { onSelect(option) },
309
+ label = { Text(option.label) },
310
+ shape = Shapes.RoundedCapPrimary,
311
+ colors =
312
+ FilterChipDefaults.filterChipColors(
313
+ containerColor = Color.Transparent,
314
+ selectedContainerColor = MaterialTheme.colorScheme.secondary,
315
+ selectedLabelColor = MaterialTheme.colorScheme.onSecondary,
316
+ ),
317
+ border =
318
+ FilterChipDefaults.filterChipBorder(
319
+ enabled = true,
320
+ selected = option.value == selectedValue,
321
+ borderColor = MaterialTheme.colorScheme.outline,
322
+ selectedBorderColor = Color.Transparent,
323
+ ),
324
+ )
325
+ }
326
+ }
327
+ }
328
+
329
+ @Composable
330
+ fun ContractSectionHeader(
331
+ title: String,
332
+ modifier: Modifier = Modifier,
333
+ ) {
334
+ Text(
335
+ text = title,
336
+ modifier = modifier.padding(horizontal = Spacing.MD, vertical = Spacing.SM),
337
+ style = MaterialTheme.typography.headlineMedium,
338
+ color = MaterialTheme.colorScheme.onSurface,
339
+ )
340
+ }
341
+
342
+ @Composable
343
+ fun ProfileHeroCard(
344
+ title: String,
345
+ subtitle: String,
346
+ body: String?,
347
+ avatarUrl: String?,
348
+ modifier: Modifier = Modifier,
349
+ actionLabel: String? = null,
350
+ actionVariant: ActionTriggerVariant = ActionTriggerVariant.Secondary,
351
+ actionIcon: (@Composable () -> Unit)? = null,
352
+ onActionClick: (() -> Unit)? = null,
353
+ ) {
354
+ Surface(
355
+ modifier = modifier,
356
+ shape = Shapes.HeroShape,
357
+ color = MaterialTheme.colorScheme.surfaceVariant,
358
+ ) {
359
+ Column(
360
+ modifier =
361
+ Modifier
362
+ .fillMaxWidth()
363
+ .padding(Spacing.LG),
364
+ verticalArrangement = Arrangement.spacedBy(Spacing.MD),
365
+ ) {
366
+ Row(
367
+ modifier = Modifier.fillMaxWidth(),
368
+ horizontalArrangement = Arrangement.spacedBy(Spacing.MD),
369
+ verticalAlignment = Alignment.CenterVertically,
370
+ ) {
371
+ RoundedCapAvatar(
372
+ imageUrl = avatarUrl,
373
+ contentDescription = title,
374
+ size = 88.dp,
375
+ )
376
+ Column(modifier = Modifier.weight(1f)) {
377
+ Text(
378
+ text = title,
379
+ style = MaterialTheme.typography.headlineLarge,
380
+ color = MaterialTheme.colorScheme.onSurface,
381
+ )
382
+ Text(
383
+ text = subtitle,
384
+ style = MaterialTheme.typography.bodyMedium,
385
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
386
+ )
387
+ }
388
+ if (actionLabel != null && onActionClick != null) {
389
+ ActionTriggerButton(
390
+ text = actionLabel,
391
+ onClick = onActionClick,
392
+ variant = actionVariant,
393
+ icon = actionIcon,
394
+ )
395
+ }
396
+ }
397
+
398
+ if (!body.isNullOrBlank()) {
399
+ Text(
400
+ text = body,
401
+ style = MaterialTheme.typography.bodyLarge,
402
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
403
+ )
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+ @Composable
410
+ fun ContractListCard(
411
+ title: String,
412
+ modifier: Modifier = Modifier,
413
+ subtitle: String? = null,
414
+ body: String? = null,
415
+ trailing: String? = null,
416
+ avatarUrl: String? = null,
417
+ onClick: (() -> Unit)? = null,
418
+ ) {
419
+ Surface(
420
+ modifier =
421
+ modifier.then(
422
+ if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier,
423
+ ),
424
+ shape = Shapes.CardShape,
425
+ color = MaterialTheme.colorScheme.surfaceVariant,
426
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
427
+ ) {
428
+ Row(
429
+ modifier =
430
+ Modifier
431
+ .fillMaxWidth()
432
+ .padding(Spacing.MD),
433
+ horizontalArrangement = Arrangement.spacedBy(Spacing.SM),
434
+ verticalAlignment = Alignment.CenterVertically,
435
+ ) {
436
+ if (avatarUrl != null) {
437
+ RoundedCapAvatar(
438
+ imageUrl = avatarUrl,
439
+ contentDescription = title,
440
+ )
441
+ }
442
+
443
+ Column(modifier = Modifier.weight(1f)) {
444
+ Text(
445
+ text = title,
446
+ style = MaterialTheme.typography.headlineSmall,
447
+ color = MaterialTheme.colorScheme.onSurface,
448
+ )
449
+ if (subtitle != null) {
450
+ Text(
451
+ text = subtitle,
452
+ style = MaterialTheme.typography.bodyMedium,
453
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
454
+ )
455
+ }
456
+ if (body != null) {
457
+ Text(
458
+ text = body,
459
+ style = MaterialTheme.typography.bodyMedium,
460
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
461
+ )
462
+ }
463
+ }
464
+
465
+ if (trailing != null) {
466
+ Text(
467
+ text = trailing,
468
+ style = MaterialTheme.typography.labelSmall,
469
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
470
+ )
471
+ }
472
+ }
473
+ }
474
+ }
475
+
476
+ @Composable
477
+ fun CompactResultRow(
478
+ title: String,
479
+ subtitle: String,
480
+ avatarUrl: String?,
481
+ modifier: Modifier = Modifier,
482
+ onClick: () -> Unit,
483
+ ) {
484
+ Column(modifier = modifier.clickable(onClick = onClick)) {
485
+ Row(
486
+ modifier =
487
+ Modifier
488
+ .fillMaxWidth()
489
+ .padding(horizontal = Spacing.MD, vertical = Spacing.SM),
490
+ horizontalArrangement = Arrangement.spacedBy(Spacing.SM),
491
+ verticalAlignment = Alignment.CenterVertically,
492
+ ) {
493
+ if (avatarUrl != null) {
494
+ RoundedCapAvatar(
495
+ imageUrl = avatarUrl,
496
+ contentDescription = title,
497
+ size = 44.dp,
498
+ )
499
+ }
500
+ Column(modifier = Modifier.weight(1f)) {
501
+ Text(
502
+ text = title,
503
+ style = MaterialTheme.typography.headlineSmall,
504
+ color = MaterialTheme.colorScheme.onSurface,
505
+ )
506
+ Text(
507
+ text = subtitle,
508
+ style = MaterialTheme.typography.bodyMedium,
509
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
510
+ )
511
+ }
512
+ }
513
+ HorizontalDivider(color = MaterialTheme.colorScheme.outline)
514
+ }
515
+ }
516
+
517
+ @Composable
518
+ fun CreatorSuggestionCard(
519
+ user: User,
520
+ modifier: Modifier = Modifier,
521
+ onClick: () -> Unit,
522
+ ) {
523
+ Surface(
524
+ modifier = modifier.clickable(onClick = onClick),
525
+ shape = Shapes.CardShape,
526
+ color = MaterialTheme.colorScheme.surfaceVariant,
527
+ border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
528
+ ) {
529
+ Column(
530
+ modifier =
531
+ Modifier
532
+ .width(180.dp)
533
+ .padding(Spacing.MD),
534
+ horizontalAlignment = Alignment.CenterHorizontally,
535
+ verticalArrangement = Arrangement.spacedBy(Spacing.SM),
536
+ ) {
537
+ RoundedCapAvatar(
538
+ imageUrl = user.avatarUrl,
539
+ contentDescription = user.displayName,
540
+ size = 64.dp,
541
+ )
542
+ Text(
543
+ text = user.displayName,
544
+ style = MaterialTheme.typography.headlineSmall,
545
+ maxLines = 1,
546
+ )
547
+ Text(
548
+ text = "@${user.handle}",
549
+ style = MaterialTheme.typography.labelSmall,
550
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
551
+ maxLines = 1,
552
+ )
553
+ }
554
+ }
555
+ }
556
+
557
+ @Composable
558
+ private fun RoundedCapAvatar(
559
+ imageUrl: String?,
560
+ contentDescription: String,
561
+ size: androidx.compose.ui.unit.Dp = 48.dp,
562
+ ) {
563
+ Box(
564
+ modifier =
565
+ Modifier
566
+ .size(size)
567
+ .clip(Shapes.RoundedCapPrimary),
568
+ ) {
569
+ if (imageUrl != null) {
570
+ AsyncImage(
571
+ model = imageUrl,
572
+ contentDescription = contentDescription,
573
+ modifier = Modifier.matchParentSize(),
574
+ contentScale = ContentScale.Crop,
575
+ )
576
+ } else {
577
+ Surface(
578
+ modifier = Modifier.matchParentSize(),
579
+ color = MaterialTheme.colorScheme.surface,
580
+ ) {
581
+ Box(contentAlignment = Alignment.Center) {
582
+ Icon(
583
+ imageVector = Icons.Default.Person,
584
+ contentDescription = null,
585
+ )
586
+ }
587
+ }
588
+ }
589
+ }
590
+ }
591
+
592
+ @Composable
593
+ fun ContractConfirmationDialog(
594
+ title: String,
595
+ message: String,
596
+ confirmLabel: String,
597
+ dismissLabel: String,
598
+ onConfirm: () -> Unit,
599
+ onDismiss: () -> Unit,
600
+ ) {
601
+ AlertDialog(
602
+ onDismissRequest = onDismiss,
603
+ shape = Shapes.HeroShape,
604
+ title = { Text(title) },
605
+ text = { Text(message) },
606
+ confirmButton = {
607
+ ActionTriggerButton(
608
+ text = confirmLabel,
609
+ onClick = onConfirm,
610
+ variant = ActionTriggerVariant.Destructive,
611
+ )
612
+ },
613
+ dismissButton = {
614
+ ActionTriggerButton(
615
+ text = dismissLabel,
616
+ onClick = onDismiss,
617
+ variant = ActionTriggerVariant.Secondary,
618
+ )
619
+ },
620
+ containerColor = MaterialTheme.colorScheme.surface,
621
+ )
622
+ }
623
+
624
+ @Composable
625
+ fun ContractSnackbarHost(
626
+ hostState: SnackbarHostState,
627
+ modifier: Modifier = Modifier,
628
+ ) {
629
+ SnackbarHost(
630
+ hostState = hostState,
631
+ modifier = modifier,
632
+ ) { snackbarData ->
633
+ Snackbar(
634
+ modifier = Modifier.padding(horizontal = Spacing.MD, vertical = Spacing.SM),
635
+ shape = Shapes.RoundedCapPrimary,
636
+ containerColor = MaterialTheme.colorScheme.primary,
637
+ contentColor = MaterialTheme.colorScheme.onPrimary,
638
+ snackbarData = snackbarData,
639
+ )
640
+ }
641
+ }