@michaelbel/cuckcoder-mcp 1.6.11

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 (92) hide show
  1. package/assets/rules/architecture.md +15 -0
  2. package/assets/rules/bottom-sheet.md +20 -0
  3. package/assets/rules/compose-color.md +22 -0
  4. package/assets/rules/compose-constraintlayout.md +18 -0
  5. package/assets/rules/compose-screen.md +33 -0
  6. package/assets/rules/compose-spacing.md +39 -0
  7. package/assets/rules/compose.md +54 -0
  8. package/assets/rules/dialog.md +33 -0
  9. package/assets/rules/domain.md +36 -0
  10. package/assets/rules/filesystem.md +3 -0
  11. package/assets/rules/git.md +11 -0
  12. package/assets/rules/github-readme.md +97 -0
  13. package/assets/rules/github-repo.md +65 -0
  14. package/assets/rules/kmp.md +14 -0
  15. package/assets/rules/kotlin.md +48 -0
  16. package/assets/rules/lazylist.md +36 -0
  17. package/assets/rules/mvi-error-handling.md +19 -0
  18. package/assets/rules/mvi-state.md +31 -0
  19. package/assets/rules/mvi.md +39 -0
  20. package/assets/rules/navigation.md +20 -0
  21. package/assets/rules/network.md +36 -0
  22. package/assets/rules/preview.md +31 -0
  23. package/assets/rules/realtime.md +39 -0
  24. package/assets/rules/resource.md +18 -0
  25. package/assets/rules/room.md +33 -0
  26. package/assets/rules/scaffold.md +25 -0
  27. package/assets/rules/shimmer.md +34 -0
  28. package/assets/rules/textfield.md +31 -0
  29. package/assets/rules/typography.md +32 -0
  30. package/assets/rules/usecase.md +60 -0
  31. package/assets/rules/workflow.md +17 -0
  32. package/assets/rules/workmanager.md +44 -0
  33. package/assets/skills/create-data-layer/SKILL.md +60 -0
  34. package/assets/skills/create-datastore-preference/SKILL.md +80 -0
  35. package/assets/skills/create-domain-mapper/SKILL.md +30 -0
  36. package/assets/skills/create-feature-alert-dialog/SKILL.md +549 -0
  37. package/assets/skills/create-feature-bottom-sheet/SKILL.md +300 -0
  38. package/assets/skills/create-feature-scaffold-screen/SKILL.md +74 -0
  39. package/assets/skills/create-feature-scaffold-screen/references/form-screen.md +20 -0
  40. package/assets/skills/create-feature-scaffold-screen/references/network-only-screen.md +20 -0
  41. package/assets/skills/create-feature-scaffold-screen/references/paginated-screen.md +29 -0
  42. package/assets/skills/create-feature-scaffold-screen/references/room-backed-screen.md +23 -0
  43. package/assets/skills/create-feature-scaffold-screen/references/static-screen.md +13 -0
  44. package/assets/skills/create-guide/SKILL.md +169 -0
  45. package/assets/skills/create-guide/references/android-project.md +120 -0
  46. package/assets/skills/create-guide/references/androidx.md +49 -0
  47. package/assets/skills/create-guide/references/existing-guides.yaml +60 -0
  48. package/assets/skills/create-guide/references/github.md +71 -0
  49. package/assets/skills/create-guide/references/guide-defaults.yaml +51 -0
  50. package/assets/skills/create-guide/references/notion.md +121 -0
  51. package/assets/skills/create-guide/references/output-contract.md +79 -0
  52. package/assets/skills/create-guide/references/research.md +64 -0
  53. package/assets/skills/create-guide/references/validation.md +59 -0
  54. package/assets/skills/create-guide/scripts/validate_guide.py +167 -0
  55. package/assets/skills/create-guide/templates/guide-manifest.yaml +67 -0
  56. package/assets/skills/create-guide/templates/implementation-plan.md +30 -0
  57. package/assets/skills/create-guide/templates/notion-page.md +46 -0
  58. package/assets/skills/create-guide/templates/repository/AGENTS.md +7 -0
  59. package/assets/skills/create-guide/templates/repository/README.md +32 -0
  60. package/assets/skills/create-guide/templates/repository/SOURCES.md +32 -0
  61. package/assets/skills/create-guide/templates/validation-report.md +30 -0
  62. package/assets/skills/create-guide/tests/test_validate_guide.py +97 -0
  63. package/assets/skills/create-ktor-endpoint/SKILL.md +33 -0
  64. package/assets/skills/create-notification-flow/SKILL.md +103 -0
  65. package/assets/skills/create-offline-outbox/SKILL.md +110 -0
  66. package/assets/skills/create-paging-flow/SKILL.md +96 -0
  67. package/assets/skills/create-project-from-template/SKILL.md +91 -0
  68. package/assets/skills/create-project-from-template/assets/icons/android.svg +12 -0
  69. package/assets/skills/create-project-from-template/assets/icons/compose.svg +34 -0
  70. package/assets/skills/create-project-from-template/assets/icons/jetpack.svg +10 -0
  71. package/assets/skills/create-project-from-template/references/myapplication-checklist.md +92 -0
  72. package/assets/skills/create-room-storage/SKILL.md +39 -0
  73. package/assets/skills/create-shared-component/SKILL.md +160 -0
  74. package/assets/skills/create-signalr-channel/SKILL.md +39 -0
  75. package/assets/skills/create-usecase/SKILL.md +142 -0
  76. package/assets/skills/create-workmanager-task/SKILL.md +41 -0
  77. package/assets/skills/github-repo-settings/SKILL.md +152 -0
  78. package/assets/skills/interview-me/SKILL.md +150 -0
  79. package/dist/errors.js +49 -0
  80. package/dist/frontmatter.js +44 -0
  81. package/dist/github.js +23 -0
  82. package/dist/index.js +6 -0
  83. package/dist/server.js +131 -0
  84. package/dist/source/bundled.js +78 -0
  85. package/dist/source/cache.js +61 -0
  86. package/dist/source/github-source.js +68 -0
  87. package/dist/source/github.js +185 -0
  88. package/dist/source/index.js +36 -0
  89. package/dist/source/types.js +1 -0
  90. package/dist/validation.js +33 -0
  91. package/dist/version.js +26 -0
  92. package/package.json +38 -0
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: create-datastore-preference
3
+ description: >-
4
+ Use when пользователь просит добавить или изменить типизированную настройку, флаг, идентификатор
5
+ или timestamp в Preferences DataStore, предоставить для него read/write/Flow use case либо
6
+ определить очистку при logout, смене пользователя или reset. Не используй для коллекций,
7
+ связанных сущностей, очередей операций и данных, которым нужны запросы или транзакции Room; для
8
+ них нужен Room-backed workflow. Не используй для полного экрана, который лишь потребляет уже
9
+ существующую настройку.
10
+ metadata:
11
+ author: michaelbel
12
+ ---
13
+
14
+ # Новая настройка DataStore
15
+
16
+ Добавляет минимальный законченный поток для небольшого сохраняемого значения: типизированный ключ,
17
+ доступ через существующий DataStore-класс, domain use case и явно выбранный жизненный цикл очистки.
18
+ Сохраняй принятую в целевом проекте структуру `PreferenceKey`, `SettingsDataStore` и DI; не создавай
19
+ вторую обёртку или новый DataStore-файл для одной настройки.
20
+
21
+ ## Определи контракт
22
+
23
+ До правок зафиксируй:
24
+
25
+ - Kotlin-тип и допустимое отсутствие значения;
26
+ - семантику отсутствия: `null`, domain default или «ещё не задавалось»;
27
+ - нужен ли разовый read, наблюдаемый `Flow`, write, remove или несколько этих операций;
28
+ - срок жизни: установка приложения, устройство, авторизованная сессия, конкретный пользователь или
29
+ временное решение фичи;
30
+ - при каких событиях значение очищается: logout, смена аккаунта, сброс настроек или никогда.
31
+
32
+ Не подменяй отсутствующее значение дефолтом в низкоуровневом DataStore-классе, если `null` несёт
33
+ domain-смысл. Выбирай default в конкретном use case, которому известна бизнес-семантика.
34
+
35
+ Для account-scoped значения выбери один контракт явно:
36
+
37
+ - session-only: храни owner id и value атомарно, считай несовпадение owner отсутствующим значением и
38
+ очищай пару при account switch;
39
+ - remembered per-account: используй безопасно namespaced key только для малого ограниченного набора
40
+ аккаунтов; для неограниченного множества и запросов по владельцу используй Room.
41
+
42
+ Один общий value key без owner недопустим: он может кратковременно отдать значение предыдущего
43
+ пользователя после смены аккаунта.
44
+
45
+ ## Получи законченный результат
46
+
47
+ 1. Добавь один типизированный `PreferenceKey<T>` с поддерживаемым Preferences DataStore типом.
48
+ Строковое имя ключа является persisted schema: для существующего значения сохраняй его буквально,
49
+ не переименовывай ради нового Kotlin-имени без явной миграции.
50
+ 2. Используй существующие generic `getValue`, `getValueFlow`, `setValue` и `removeValue`, если они уже
51
+ покрывают операцию. Расширяй `SettingsDataStore` только когда требуется новая атомарная операция,
52
+ а не ради feature-specific метода.
53
+ 3. Размести чтение, запись и бизнес-решения в конкретных `UseCase` / `FlowUseCase` из
54
+ `shared/domain/usecase`; внедри `SettingsDataStore` напрямую. UI и ViewModel не должны знать
55
+ DataStore key или обращаться к DataStore напрямую.
56
+ 4. Для read-modify-write нескольких значений проверь, требуется ли одна атомарная `edit`-транзакция.
57
+ Не собирай несколько независимых `setValue` в use case, если наблюдатель не должен видеть
58
+ промежуточное состояние.
59
+ 5. Подключи удаление к владельцу lifecycle-события. На logout очищай session-only значения, но не
60
+ удаляй remembered per-account и device-scoped настройки. При смене пользователя сначала смени
61
+ owner scope, затем разрешай чтение account-scoped значения.
62
+
63
+ ## Границы реализации
64
+
65
+ - Используй разовый suspend read для решения, принимаемого один раз, и `FlowUseCase` для состояния,
66
+ которое должно обновлять потребителя после записи.
67
+ - Не вводи blocking read в новую логику. Сохраняй существующий blocking API только там, где
68
+ синхронную инициализацию уже требует инфраструктура приложения.
69
+ - Не сериализуй коллекцию или сложный изменяемый объект в строку только для обхода Room. Допускай
70
+ кодированное значение лишь для небольшого стабильного типа и только если такой формат уже принят
71
+ проектом.
72
+ - Не создавай Repository или Interactor и не переносись на Proto DataStore без отдельного запроса.
73
+ - Если несколько ключей разных типов очищаются вместе, операция очистки должна поддерживать их без
74
+ небезопасного общего generic-типа.
75
+
76
+ ## Проверка результата
77
+
78
+ Проверь cold start без значения, чтение после записи, обновление Flow, удаление на каждом выбранном
79
+ lifecycle-событии и сохранение ключей другого срока жизни. Для существующего ключа отдельно проверь,
80
+ что строковое persisted-имя не изменилось.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: create-domain-mapper
3
+ description: >-
4
+ Use when пользователь просит создать или изменить mapper между network, Room и domain-моделями
5
+ либо KTX-функции `entity`/`entities`. Не используй для создания самих API-моделей, DAO, endpoint
6
+ или бизнес-операции use case.
7
+ metadata:
8
+ author: michaelbel
9
+ ---
10
+
11
+ # Создание domain-mapper
12
+
13
+ Изучи соседние `shared/domain/mapper/*Ktx.kt` и помести преобразование в тематический KTX-файл. Не
14
+ создавай stateful `Mapper`-класс и не переноси mapping во ViewModel или use case.
15
+
16
+ ## Контракт
17
+
18
+ - Называй `Response -> Entity` extension-функцию `entity`, а преобразование списка — `entities`.
19
+ - Для другого направления используй короткое имя целевого типа или существующее соглашение проекта.
20
+ - Размещай параметры функции и аргументы конструктора целевой модели по одному на строку.
21
+ - Передавай внешние значения, которых нет в source-модели, явными параметрами mapper; не читай
22
+ состояние из service locator или singleton.
23
+ - Возвращай non-null entity. Nullable поля ответа заполняй через `orEmpty`, включая nullable
24
+ примитивы из `PrimitiveKtx.kt`; добавь недостающее primitive-extension туда.
25
+ - Не используй `return null` или `mapNotNull` для молчаливого удаления некорректных ответов. Если
26
+ значение нельзя корректно восстановить, оставь валидацию и domain-ошибку вызывающей операции.
27
+ - Не создавай отдельную UI-модель, если entity/domain-модель уже выражает нужное состояние.
28
+
29
+ Добавь focused unit-тест mapper, когда преобразование содержит ветвление, вложенные коллекции,
30
+ форматирование или нетривиальные default-значения.
@@ -0,0 +1,549 @@
1
+ ---
2
+ name: create-feature-alert-dialog
3
+ description: >-
4
+ Use when пользователь просит создать в Compose диалог подтверждения, alert dialog, picker dialog
5
+ или простой status popup с явным действием confirm/dismiss, либо говорит "create a dialog", "add
6
+ an alert dialog", "confirmation popup", "new AlertDialog". Строит `AlertDialog` с одной или двумя
7
+ кнопками для подтверждающих сценариев и `BasicAlertDialog` — для пикера без кнопок действия. Если
8
+ нужна прокручиваемая модальная поверхность, закреплённая снизу экрана, используй
9
+ [create-feature-bottom-sheet](../create-feature-bottom-sheet/SKILL.md) вместо этого скилла. Для
10
+ полного экрана используй
11
+ [create-feature-scaffold-screen](../create-feature-scaffold-screen/SKILL.md), для простого
12
+ переиспользуемого компонента без диалоговой семантики —
13
+ [create-shared-component](../create-shared-component/SKILL.md).
14
+ metadata:
15
+ author: michaelbel
16
+ ---
17
+
18
+ # Новый Alert Dialog
19
+
20
+ Требует зависимость `androidx.compose.material3:material3`.
21
+
22
+ Создаёт Compose-диалог проекта. Замени `{Feature}` на назначение диалога, `{feature}` на имя в
23
+ lower camel case, а `{package}` на целевой пакет.
24
+
25
+ Используй `AlertDialog`, когда в диалоге есть кнопки действий (с текстом, иконкой или списком в
26
+ слоте `text`). Используй `BasicAlertDialog`, когда кнопок действий нет.
27
+
28
+ ## Фаза 1: создать папку фичи
29
+
30
+ Создай папку `features/{feature}_dialog` — постфикс `_dialog` обязателен, даже если фича сама по
31
+ себе не заканчивается на «диалог».
32
+
33
+ ## Фаза 2: Intent и Model
34
+
35
+ Внутри `features/{feature}_dialog` создай папку `intent`. Папку `model` создавай только когда
36
+ диалогу нужны отображаемые данные; если данных нет, не создавай ни папку, ни файл Model.
37
+
38
+ Файлы:
39
+ - `features/{feature}_dialog/{Feature}Dialog.kt`
40
+ - `features/{feature}_dialog/intent/{Feature}Intent.kt`
41
+ - optional `features/{feature}_dialog/model/{Feature}Model.kt`
42
+
43
+ ### {Feature}Intent.kt
44
+
45
+ ```kotlin
46
+ package {package}.features.{feature}_dialog.intent
47
+
48
+ import {package}.shared.mvi.Intent
49
+
50
+ sealed interface {Feature}Intent: Intent {
51
+ data object DismissClick: {Feature}Intent
52
+ }
53
+ ```
54
+
55
+ Правила:
56
+ - `DismissClick` присутствует всегда.
57
+ - Если у диалога есть кнопка подтверждения, добавь `data object ConfirmClick: {Feature}Intent`.
58
+ - Для остальных действий (выбор опции, клик по элементу и т. д.) добавляй свои `data object` /
59
+ `data class` записи; `data object` всегда идёт перед `data class`.
60
+
61
+ ### {Feature}Model.kt (только если есть данные)
62
+
63
+ ```kotlin
64
+ package {package}.features.{feature}_dialog.model
65
+
66
+ import {package}.shared.mvi.Model
67
+
68
+ data class {Feature}Model(
69
+ val selectedOptionId: String = ""
70
+ ): Model
71
+ ```
72
+
73
+ ## AlertDialog с заголовком и двумя кнопками (без Model)
74
+
75
+ ```kotlin
76
+ package {package}.features.{feature}_dialog
77
+
78
+ // Добавь все необходимые импорты
79
+
80
+ @Composable
81
+ fun {Feature}Dialog(
82
+ dispatch: ({Feature}Intent) -> Unit
83
+ ) {
84
+ AlertDialog(
85
+ onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
86
+ confirmButton = {
87
+ TextButton(
88
+ onClick = {
89
+ dispatch({Feature}Intent.ConfirmClick)
90
+ dispatch({Feature}Intent.DismissClick)
91
+ }
92
+ ) {
93
+ Text(
94
+ text = stringResource(AppStrings.{Feature}Confirm),
95
+ style = MaterialTheme.typography.medium14
96
+ )
97
+ }
98
+ },
99
+ dismissButton = {
100
+ TextButton(
101
+ onClick = { dispatch({Feature}Intent.DismissClick) }
102
+ ) {
103
+ Text(
104
+ text = stringResource(AppStrings.DialogCancel),
105
+ style = MaterialTheme.typography.regular14
106
+ )
107
+ }
108
+ },
109
+ title = {
110
+ Text(
111
+ text = stringResource(AppStrings.{Feature}Title),
112
+ style = MaterialTheme.typography.regular22.copy(
113
+ lineHeight = 22.sp
114
+ )
115
+ )
116
+ },
117
+ text = {
118
+ Text(
119
+ text = stringResource(AppStrings.{Feature}Message),
120
+ style = MaterialTheme.typography.regular14
121
+ )
122
+ },
123
+ titleContentColor = MaterialTheme.colorScheme.onBackground,
124
+ textContentColor = MaterialTheme.colorScheme.onSurfaceVariant2
125
+ )
126
+ }
127
+
128
+ @PreviewWrapper(ThemeWrapper::class)
129
+ @Preview
130
+ @Composable
131
+ private fun {Feature}DialogPreview() {
132
+ Box(
133
+ modifier = Modifier.fillMaxSize()
134
+ ) {
135
+ {Feature}Dialog(
136
+ dispatch = {}
137
+ )
138
+ }
139
+ }
140
+ ```
141
+
142
+ ## AlertDialog с одной кнопкой (иконка опциональна)
143
+
144
+ ```kotlin
145
+ package {package}.features.{feature}_dialog
146
+
147
+ // Добавь все необходимые импорты
148
+
149
+ @Composable
150
+ fun {Feature}Dialog(
151
+ dispatch: ({Feature}Intent) -> Unit
152
+ ) {
153
+ AlertDialog(
154
+ onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
155
+ confirmButton = {
156
+ TextButton(
157
+ onClick = { dispatch({Feature}Intent.DismissClick) }
158
+ ) {
159
+ Text(
160
+ text = stringResource(AppStrings.DialogOk),
161
+ style = MaterialTheme.typography.medium14
162
+ )
163
+ }
164
+ },
165
+ icon = {
166
+ Icon(
167
+ imageVector = {IconName},
168
+ contentDescription = null,
169
+ modifier = Modifier.size(44.dp)
170
+ )
171
+ },
172
+ title = {
173
+ Text(
174
+ text = stringResource(AppStrings.{Feature}Title),
175
+ style = MaterialTheme.typography.regular22
176
+ )
177
+ },
178
+ text = {
179
+ Text(
180
+ text = stringResource(AppStrings.{Feature}Message),
181
+ style = MaterialTheme.typography.regular14
182
+ )
183
+ },
184
+ iconContentColor = MaterialTheme.colorScheme.error,
185
+ titleContentColor = MaterialTheme.colorScheme.error,
186
+ textContentColor = MaterialTheme.colorScheme.onBackground
187
+ )
188
+ }
189
+
190
+ @PreviewWrapper(ThemeWrapper::class)
191
+ @Preview
192
+ @Composable
193
+ private fun {Feature}DialogPreview() {
194
+ Box(
195
+ modifier = Modifier.fillMaxSize()
196
+ ) {
197
+ {Feature}Dialog(
198
+ dispatch = {}
199
+ )
200
+ }
201
+ }
202
+ ```
203
+
204
+ Убери блок `icon` целиком и `iconContentColor`, если иконка не нужна; тогда `titleContentColor` /
205
+ `textContentColor` возвращаются к обычным `onBackground` / `onSurfaceVariant2`. Для диалогов
206
+ ошибок/предупреждений задавай `iconContentColor` и `titleContentColor` в
207
+ `MaterialTheme.colorScheme.error`.
208
+
209
+ ## AlertDialog со списком в тексте (с Model)
210
+
211
+ Когда нужен диалог с заголовком, кнопками и выбираемым списком, размещай список внутри слота
212
+ `text`, а не переключайся на `BasicAlertDialog`.
213
+
214
+ ```kotlin
215
+ package {package}.features.{feature}_dialog
216
+
217
+ // Добавь все необходимые импорты
218
+
219
+ @Composable
220
+ fun {Feature}Dialog(
221
+ state: {Feature}Model,
222
+ dispatch: ({Feature}Intent) -> Unit
223
+ ) {
224
+ AlertDialog(
225
+ onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
226
+ confirmButton = {
227
+ TextButton(
228
+ onClick = {
229
+ dispatch({Feature}Intent.ConfirmClick)
230
+ dispatch({Feature}Intent.DismissClick)
231
+ },
232
+ enabled = state.selectedOptionId.isNotEmpty()
233
+ ) {
234
+ Text(
235
+ text = stringResource(AppStrings.DialogChoose),
236
+ style = MaterialTheme.typography.medium14
237
+ )
238
+ }
239
+ },
240
+ dismissButton = {
241
+ TextButton(
242
+ onClick = { dispatch({Feature}Intent.DismissClick) }
243
+ ) {
244
+ Text(
245
+ text = stringResource(AppStrings.DialogCancel),
246
+ style = MaterialTheme.typography.regular14
247
+ )
248
+ }
249
+ },
250
+ text = {
251
+ Column {
252
+ Text(
253
+ text = stringResource(AppStrings.{Feature}Title),
254
+ style = MaterialTheme.typography.regular22
255
+ )
256
+
257
+ Spacer(
258
+ modifier = Modifier.height(24.dp)
259
+ )
260
+
261
+ state.options.forEach { option ->
262
+ Row(
263
+ modifier = Modifier
264
+ .fillMaxWidth()
265
+ .height(56.dp)
266
+ .clickable { dispatch({Feature}Intent.OptionClick(option.id)) },
267
+ horizontalArrangement = Arrangement.SpaceBetween,
268
+ verticalAlignment = Alignment.CenterVertically
269
+ ) {
270
+ Text(
271
+ text = option.title,
272
+ style = MaterialTheme.typography.regular14
273
+ )
274
+
275
+ RadioButton(
276
+ selected = state.selectedOptionId == option.id,
277
+ onClick = null
278
+ )
279
+ }
280
+
281
+ HorizontalDivider(
282
+ modifier = Modifier.fillMaxWidth(),
283
+ color = MaterialTheme.colorScheme.outlineVariant
284
+ )
285
+ }
286
+ }
287
+ },
288
+ titleContentColor = MaterialTheme.colorScheme.onBackground,
289
+ textContentColor = MaterialTheme.colorScheme.onSurfaceVariant2
290
+ )
291
+ }
292
+
293
+ @PreviewWrapper(ThemeWrapper::class)
294
+ @Preview
295
+ @Composable
296
+ private fun {Feature}DialogPreview(
297
+ @PreviewParameter({Feature}ModelPreviewParameterProvider::class) state: {Feature}Model
298
+ ) {
299
+ Box(
300
+ modifier = Modifier.fillMaxSize()
301
+ ) {
302
+ {Feature}Dialog(
303
+ state = state,
304
+ dispatch = {}
305
+ )
306
+ }
307
+ }
308
+
309
+ private class {Feature}ModelPreviewParameterProvider: PreviewParameterProvider<{Feature}Model> {
310
+ private val options = listOf(
311
+ {Feature}Option(id = "sample-a", title = "Sample A"),
312
+ {Feature}Option(id = "sample-b", title = "Sample B"),
313
+ {Feature}Option(id = "sample-c", title = "Sample C")
314
+ )
315
+
316
+ override val values: Sequence<{Feature}Model>
317
+ get() = sequenceOf(
318
+ {Feature}Model(options = options, selectedOptionId = "sample-a"),
319
+ {Feature}Model(options = options, selectedOptionId = "")
320
+ )
321
+ }
322
+ ```
323
+
324
+ ## BasicAlertDialog без кнопок
325
+
326
+ ```kotlin
327
+ @file:OptIn(ExperimentalMaterial3Api::class)
328
+
329
+ package {package}.features.{feature}_dialog
330
+
331
+ // Добавь все необходимые импорты
332
+
333
+ @Composable
334
+ fun {Feature}Dialog(
335
+ state: {Feature}Model,
336
+ dispatch: ({Feature}Intent) -> Unit
337
+ ) {
338
+ BasicAlertDialog(
339
+ onDismissRequest = { dispatch({Feature}Intent.DismissClick) },
340
+ modifier = Modifier
341
+ .fillMaxWidth()
342
+ .wrapContentHeight()
343
+ .clip(RoundedCornerShape(16.dp))
344
+ .background(MaterialTheme.colorScheme.surfaceContainerHigh)
345
+ ) {
346
+ Column(
347
+ modifier = Modifier.padding(vertical = 24.dp),
348
+ verticalArrangement = Arrangement.spacedBy(2.dp),
349
+ horizontalAlignment = Alignment.Start
350
+ ) {
351
+ Text(
352
+ text = stringResource(AppStrings.{Feature}Title),
353
+ modifier = Modifier.padding(horizontal = 24.dp),
354
+ style = MaterialTheme.typography.titleText
355
+ )
356
+
357
+ Spacer(
358
+ modifier = Modifier.height(12.dp)
359
+ )
360
+
361
+ state.options.forEachIndexed { index, option ->
362
+ SegmentedListItem(
363
+ modifier = Modifier
364
+ .padding(horizontal = 16.dp)
365
+ .clickable {
366
+ dispatch({Feature}Intent.OptionClick(option))
367
+ dispatch({Feature}Intent.DismissClick)
368
+ },
369
+ headlineContent = {
370
+ Text(
371
+ text = option.title,
372
+ style = MaterialTheme.typography.headlineText
373
+ )
374
+ },
375
+ trailingContent = {
376
+ RadioButton(
377
+ selected = state.selectedOption == option,
378
+ onClick = null
379
+ )
380
+ },
381
+ colors = ListItemDefaults.colors().copy(
382
+ containerColor = MaterialTheme.colorScheme.surface,
383
+ headlineColor = MaterialTheme.colorScheme.onBackground,
384
+ supportingTextColor = MaterialTheme.colorScheme.secondary,
385
+ trailingIconColor = MaterialTheme.colorScheme.onBackground
386
+ )
387
+ )
388
+ }
389
+ }
390
+ }
391
+ }
392
+
393
+ @PreviewWrapper(ThemeWrapper::class)
394
+ @Preview
395
+ @Composable
396
+ private fun {Feature}Preview() {
397
+ Box(
398
+ modifier = Modifier.fillMaxSize()
399
+ ) {
400
+ {Feature}Dialog(
401
+ state = {Feature}Model(
402
+ selectedOption = {Feature}Option.SampleA,
403
+ options = listOf(
404
+ {Feature}Option.SampleA,
405
+ {Feature}Option.SampleB,
406
+ {Feature}Option.SampleC
407
+ )
408
+ ),
409
+ dispatch = {}
410
+ )
411
+ }
412
+ }
413
+ ```
414
+
415
+ ## Фаза 3: вызов диалога с экрана
416
+
417
+ Диалог не хранит собственный ViewModel — видимостью и данными управляет экран, который его
418
+ открывает.
419
+
420
+ ### 1. Флаг видимости в Model экрана
421
+
422
+ В `Model` вызывающего экрана добавь `Boolean`-поле `is{Feature}DialogVisible` — `is`, имя диалога
423
+ (с постфиксом `Dialog`), затем `Visible`:
424
+
425
+ ```kotlin
426
+ data class {Screen}Model(
427
+ val is{Feature}DialogVisible: Boolean = false
428
+ ): Model
429
+ ```
430
+
431
+ ### 2. Показ диалога
432
+
433
+ Там, где решаешь открыть диалог, диспатчи `reduce`, выставляющий флаг в `true`:
434
+
435
+ ```kotlin
436
+ reduce { it.copy(is{Feature}DialogVisible = true) }
437
+ ```
438
+
439
+ ### 3. Обработка intent'ов диалога в ViewModel экрана
440
+
441
+ Добавь в `{Screen}Intent` экрана один case `On{Feature}Intent`, оборачивающий весь `{Feature}Intent`
442
+ диалога целиком — отдельный case на каждый intent диалога не создавай:
443
+
444
+ ```kotlin
445
+ sealed interface {Screen}Intent: Intent {
446
+ data class On{Feature}Intent(val intent: {Feature}Intent): {Screen}Intent
447
+ }
448
+ ```
449
+
450
+ Разбирай `intent.intent` вложенным `when` внутри `dispatch` ViewModel экрана:
451
+
452
+ ```kotlin
453
+ override fun dispatch(intent: {Screen}Intent) {
454
+ when (intent) {
455
+ is {Screen}Intent.On{Feature}Intent -> {
456
+ when (intent.intent) {
457
+ is {Feature}Intent.DismissClick -> {
458
+ reduce { it.copy(is{Feature}DialogVisible = false) }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+ ```
465
+
466
+ ### 4. Отрисовка
467
+
468
+ Рендери диалог внутри первой, публичной функции экрана (`{Screen}Screen`, не
469
+ `{Screen}ScreenContent`), сразу после вызова `{Screen}ScreenContent(...)`. В `dispatch`-лямбде
470
+ диалога просто пробрасывай весь intent в `On{Feature}Intent`, не разбирая его case'ы в самом
471
+ composable:
472
+
473
+ ```kotlin
474
+ @Composable
475
+ fun {Screen}Screen(
476
+ viewModel: {Screen}ViewModel = hiltViewModel()
477
+ ) {
478
+ val state by viewModel.stateFlow.collectAsStateWithLifecycle()
479
+
480
+ {Screen}ScreenContent(
481
+ state = state,
482
+ dispatch = viewModel::dispatch
483
+ )
484
+
485
+ if (state.is{Feature}DialogVisible) {
486
+ {Feature}Dialog(
487
+ dispatch = { intent -> viewModel.dispatch({Screen}Intent.On{Feature}Intent(intent)) }
488
+ )
489
+ }
490
+ }
491
+ ```
492
+
493
+ ### Если у диалога есть Model
494
+
495
+ Когда диалог принимает `state`, не храни отдельную копию его Model в экране — вычисляй её
496
+ свойством `get()` из уже существующих данных экрана. Имя свойства оканчивается на `State`, а не на
497
+ `Model` (сам тип остаётся `{Feature}Model`):
498
+
499
+ ```kotlin
500
+ data class {Screen}Model(
501
+ val {feature}Message: String = "",
502
+ val is{Feature}DialogVisible: Boolean = false
503
+ ): Model {
504
+ val {feature}State: {Feature}Model
505
+ get() = {Feature}Model(
506
+ message = {feature}Message
507
+ )
508
+ }
509
+ ```
510
+
511
+ ```kotlin
512
+ if (state.is{Feature}DialogVisible) {
513
+ {Feature}Dialog(
514
+ state = state.{feature}State,
515
+ dispatch = { intent -> viewModel.dispatch({Screen}Intent.On{Feature}Intent(intent)) }
516
+ )
517
+ }
518
+ ```
519
+
520
+ Правила:
521
+ - Используй `AlertDialog`, когда в диалоге есть кнопки действий (с текстом, иконкой или списком в
522
+ `text`). Используй `BasicAlertDialog` только для пикера без кнопок действия.
523
+ - Для `BasicAlertDialog` применяй `clip` и `background` прямо на modifier диалога.
524
+ - На `AlertDialog` всегда указывай `titleContentColor` и `textContentColor` — обычно
525
+ `MaterialTheme.colorScheme.onBackground` и `MaterialTheme.colorScheme.onSurfaceVariant2`; для
526
+ диалогов ошибок/предупреждений — `MaterialTheme.colorScheme.error`.
527
+ - Sealed interface `{Feature}Intent` всегда содержит `DismissClick`; добавляй `ConfirmClick`,
528
+ когда есть кнопка подтверждения.
529
+ - Создавай `{Feature}Model` только когда диалогу нужны отображаемые данные; иначе не создавай ни
530
+ папку, ни файл `model`.
531
+ - Указывай `@file:OptIn(...)` перед `package`, когда используешь experimental API (например
532
+ `BasicAlertDialog` требует `ExperimentalMaterial3Api`) или любой другой opt-in, нужный коду.
533
+ - Делай preview каждого диалога внутри `Box(modifier = Modifier.fillMaxSize())`; `showBackground =
534
+ true` не указывай.
535
+ - Для preview с состоянием всегда создавай приватный `{Feature}ModelPreviewParameterProvider:
536
+ PreviewParameterProvider<{Feature}Model>` в том же файле и передавай его через
537
+ `@PreviewParameter`. Preview без состояния параметров не принимает.
538
+ - В лямбдах кнопки подтверждения диспатчи intent подтверждения перед intent закрытия.
539
+ - Используй анонимизированные значения для preview, такие как `0000`, `SampleA`, `SampleB` и
540
+ `SampleC`.
541
+ - Видимостью диалога управляет вызывающий экран через `Boolean`-поле `is{Feature}DialogVisible` в
542
+ своей `Model`; у самого диалога отдельной ViewModel нет.
543
+ - Экран оборачивает весь `{Feature}Intent` диалога в один case `On{Feature}Intent(val intent:
544
+ {Feature}Intent)` своего `{Screen}Intent`; отдельных case'ов на каждый intent диалога не
545
+ создавай — разбирай их вложенным `when` внутри `dispatch` ViewModel.
546
+ - Рендери диалог в первой, публичной функции экрана, сразу после вызова
547
+ `{Screen}ScreenContent(...)`.
548
+ - Если диалогу нужна Model, вычисляй её свойством `get()` в `Model` экрана из уже существующих
549
+ данных — не храни отдельную копию; имя свойства оканчивается на `State`, а не на `Model`.