@shaykec/app-agent 1.0.9 → 1.0.10
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/.claude/agents/catalog-analyzer.md +57 -0
- package/.claude/skills/android-customizer/SKILL.md +23 -10
- package/.claude/skills/bug-fixer/SKILL.md +59 -0
- package/.claude/skills/catalog-analyzer/SKILL.md +96 -0
- package/.claude/skills/customization-planner/SKILL.md +44 -5
- package/.claude/skills/design-selector/SKILL.md +3 -1
- package/.claude/skills/design-system/SKILL.md +1 -1
- package/.claude/skills/exploratory-tester/SKILL.md +82 -0
- package/.claude/skills/ios-customizer/SKILL.md +29 -8
- package/.claude/skills/module-integrator/SKILL.md +1 -1
- package/.claude/skills/react-native-customizer/SKILL.md +22 -10
- package/.claude/skills/test-planner/SKILL.md +72 -0
- package/.cursor/agents/README.md +3 -1
- package/.cursor/agents/catalog-analyzer.md +83 -0
- package/.cursor/rules/safety-guardrails.mdc +1 -1
- package/.cursor/rules/workflow.mdc +52 -18
- package/.cursor/skills/android-customizer/SKILL.md +43 -19
- package/.cursor/skills/bug-fixer/SKILL.md +189 -0
- package/.cursor/skills/catalog-analyzer/SKILL.md +222 -0
- package/.cursor/skills/customization-planner/SKILL.md +55 -8
- package/.cursor/skills/design-selector/SKILL.md +6 -5
- package/.cursor/skills/design-system/SKILL.md +8 -7
- package/.cursor/skills/exploratory-tester/SKILL.md +223 -0
- package/.cursor/skills/ios-customizer/SKILL.md +47 -12
- package/.cursor/skills/module-integrator/SKILL.md +2 -2
- package/.cursor/skills/output-validator/SKILL.md +1 -1
- package/.cursor/skills/react-native-customizer/SKILL.md +46 -16
- package/.cursor/skills/test-planner/SKILL.md +199 -0
- package/AGENTS.md +32 -11
- package/CLAUDE.md +78 -33
- package/README.md +77 -11
- package/designs/DESIGN_CATALOG.md +17 -15
- package/designs/DESIGN_PRINCIPLES.md +53 -0
- package/designs/brands/accessible-high-contrast.md +14 -0
- package/designs/brands/corporate-professional.md +14 -0
- package/designs/brands/dark-luxe.md +14 -0
- package/designs/brands/kids-playful.md +14 -0
- package/designs/brands/medical-clinical.md +14 -0
- package/designs/brands/modern-minimal.md +14 -0
- package/designs/brands/nature-organic.md +14 -0
- package/designs/brands/neo-brutalist.md +14 -0
- package/designs/brands/retro-vintage.md +14 -0
- package/designs/brands/soft-gradient.md +14 -0
- package/designs/brands/sport-athletic.md +14 -0
- package/designs/brands/tech-dynamic.md +14 -0
- package/designs/brands/vibrant-playful.md +14 -0
- package/dist/cli.d.ts +4 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/engines/claude-engine.d.ts.map +1 -1
- package/dist/engines/claude-engine.js +16 -4
- package/dist/engines/claude-engine.js.map +1 -1
- package/dist/engines/types.d.ts +1 -1
- package/dist/engines/types.d.ts.map +1 -1
- package/dist/engines/types.js +31 -2
- package/dist/engines/types.js.map +1 -1
- package/dist/github.d.ts +3 -0
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +47 -4
- package/dist/github.js.map +1 -1
- package/dist/index.js +217 -9
- package/dist/index.js.map +1 -1
- package/dist/prompt-builder.d.ts +11 -1
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +216 -1
- package/dist/prompt-builder.js.map +1 -1
- package/dist/validator.d.ts +7 -2
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +61 -41
- package/dist/validator.js.map +1 -1
- package/dist/workspace.js +2 -2
- package/dist/workspace.js.map +1 -1
- package/package.json +2 -2
- package/prompts/agent-prompt.md +35 -18
- package/prompts/deep-test-agent-prompt.md +122 -0
- package/prompts/fix-agent-prompt.md +90 -0
- package/prompts/quick-agent-prompt.md +32 -2
- package/prompts/scratch-agent-prompt.md +5 -8
- package/templates/android/BookTemplate/app/src/main/kotlin/com/appship/book/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ChatTemplate/app/src/main/kotlin/com/appship/chat/features/conversations/ConversationsScreen.kt +1 -1
- package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/DashTemplate/app/src/main/kotlin/com/appship/dash/features/navigation/MainScreen.kt +1 -0
- package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/FamilyTemplate/app/src/main/java/com/appship/family/features/navigation/MainNavigation.kt +5 -1
- package/templates/android/FinanceTemplate/app/src/main/kotlin/com/appship/finance/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/core/animation/MotionPreferencesScreen.kt +3 -3
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/navigation/Navigation.kt +1 -1
- package/templates/android/GameTemplate/app/src/main/kotlin/com/appship/game/features/settings/SettingsScreen.kt +1 -1
- package/templates/android/HealthTemplate/app/src/main/kotlin/com/appship/health/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/LearnTemplate/app/src/main/kotlin/com/appship/learn/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/MapTemplate/app/src/main/kotlin/com/appship/map/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/MediaTemplate/app/src/main/kotlin/com/appship/media/features/settings/SettingsScreen.kt +3 -2
- package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ReferenceTemplate/app/src/main/kotlin/com/appship/reference/features/settings/SettingsScreen.kt +1 -1
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/ShopTemplate/app/src/main/kotlin/com/appship/shop/features/cart/CartScreen.kt +3 -2
- package/templates/android/Skeleton/app/src/main/kotlin/com/appship/skeleton/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/Skeleton/tests/03_detail_screen.yaml +1 -1
- package/templates/android/Skeleton/tests/04_favorites.yaml +1 -1
- package/templates/android/Skeleton/tests/08_full_e2e.yaml +7 -1
- package/templates/android/SocialTemplate/app/src/main/kotlin/com/appship/social/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/android/TaskTemplate/app/src/main/kotlin/com/appship/task/features/settings/SettingsScreen.kt +3 -2
- package/templates/android/TrackTemplate/app/src/main/kotlin/com/appship/track/core/animation/AnimatedTransitionsModifiers.kt +188 -0
- package/templates/ios/BookTemplate/BookTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/ChatTemplate/ChatTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/DashTemplate/DashTemplate/App/AppConfig.swift +1 -0
- package/templates/ios/DashTemplate/DashTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/DashTemplate/DashTemplate/Core/Strings.swift +13 -0
- package/templates/ios/DashTemplate/DashTemplate.xcodeproj/project.pbxproj +32 -20
- package/templates/ios/FamilyTemplate/FamilyTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/FinanceTemplate/FinanceTemplate/Core/Strings.swift +42 -0
- package/templates/ios/FinanceTemplate/FinanceTemplate.xcodeproj/project.pbxproj +36 -30
- package/templates/ios/GameTemplate/GameTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/HealthTemplate/HealthTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/LearnTemplate/LearnTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/MapTemplate/MapTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/MediaTemplate/MediaTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/ReferenceTemplate/ReferenceTemplate/Core/Strings.swift +12 -0
- package/templates/ios/ReferenceTemplate/ReferenceTemplate/Features/SkeletonLoading/SkeletonLoadingView.swift +2 -37
- package/templates/ios/ShopTemplate/ShopTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/Skeleton/Skeleton/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/Skeleton/tests/08_full_e2e.yaml +4 -0
- package/templates/ios/SocialTemplate/SocialTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/TaskTemplate/TaskTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/ios/TrackTemplate/TrackTemplate/Core/Animation/AnimatedTransitionsView.swift +201 -0
- package/templates/react-native/BookTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/BookTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/BookTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/ChatTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/ChatTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/ChatTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/DashTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/DashTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/DashTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/FamilyTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/FamilyTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/FamilyTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/FinanceTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/FinanceTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/FinanceTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/GameTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/GameTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/GameTemplate/src/screens/GameDetail/GameDetailScreen.tsx +2 -1
- package/templates/react-native/GameTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/HealthTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/HealthTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/HealthTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/HealthTemplate/src/screens/WorkoutDetail/WorkoutDetailScreen.tsx +1 -1
- package/templates/react-native/LearnTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/LearnTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/LearnTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/MapTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/MapTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/MapTemplate/src/screens/Map/MapScreen.tsx +14 -0
- package/templates/react-native/MapTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/MediaTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/MediaTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/MediaTemplate/src/screens/PlaylistDetail/PlaylistDetailScreen.tsx +1 -1
- package/templates/react-native/MediaTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/ReferenceTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/ReferenceTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/ReferenceTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
- package/templates/react-native/ShopTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/ShopTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/ShopTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/Skeleton/TESTING_MANIFEST.md +1 -1
- package/templates/react-native/Skeleton/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/Skeleton/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/Skeleton/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/Skeleton/tests/07_profile.yaml +3 -2
- package/templates/react-native/Skeleton/tests/08_full_e2e.yaml +12 -1
- package/templates/react-native/SocialTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/SocialTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/SocialTemplate/src/screens/Feed/FeedScreen.tsx +1 -0
- package/templates/react-native/SocialTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/TaskTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/TaskTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/TaskTemplate/src/screens/Profile/ProfileScreen.tsx +1 -1
- package/templates/react-native/TrackTemplate/src/animation/useAnimatedList.ts +219 -2
- package/templates/react-native/TrackTemplate/src/animation/useMotionPreferences.ts +23 -9
- package/templates/react-native/TrackTemplate/src/screens/Settings/SettingsScreen.tsx +1 -1
- package/templates/shared/ios/AnimatedTransitions/AnimatedTransitionsView.swift +233 -93
- package/.claude/agents/template-selector.md +0 -39
- package/.claude/skills/module-selector/SKILL.md +0 -81
- package/.claude/skills/template-selector/SKILL.md +0 -44
- package/.cursor/agents/template-selector.md +0 -52
- package/.cursor/skills/module-selector/SKILL.md +0 -135
- package/.cursor/skills/template-selector/SKILL.md +0 -123
|
@@ -22,9 +22,8 @@ You have specialised skills available in `.cursor/skills/`. Each skill contains
|
|
|
22
22
|
| Step | Skill | Purpose |
|
|
23
23
|
|------|-------|---------|
|
|
24
24
|
| 0 | `prompt-validator` | Validate user prompt for injection/malicious intent |
|
|
25
|
-
| 0 | `
|
|
25
|
+
| 0 | `catalog-analyzer` | Select 1-3 reference templates from CATALOG.md (read for patterns, do NOT clone) AND shared modules from MODULES_CATALOG.md |
|
|
26
26
|
| 0 | `design-selector` | Select the best design brand from DESIGN_CATALOG.md |
|
|
27
|
-
| 0 | `module-selector` | Select shared modules from MODULES_CATALOG.md based on app keywords |
|
|
28
27
|
| 1 | `content-writer` | Generate all user-facing strings (used during architecture + content planning) |
|
|
29
28
|
| 3 | `module-integrator` | Read shared module references and write adapted code into the app |
|
|
30
29
|
| 4 | `design-system` | Apply the selected brand's tokens to the app |
|
|
@@ -41,8 +40,7 @@ Every skill writes a report file to `output/{app-name}/reports/`. These reports
|
|
|
41
40
|
```
|
|
42
41
|
output/{app-name}/reports/
|
|
43
42
|
01-prompt-validation.md
|
|
44
|
-
02-
|
|
45
|
-
02b-module-selection.md
|
|
43
|
+
02-catalog-analysis.md
|
|
46
44
|
03-design-brand.md
|
|
47
45
|
architecture-plan.md
|
|
48
46
|
04-content-brief.md
|
|
@@ -83,14 +81,13 @@ Follow these steps in order. Steps marked `[PARALLEL]` should use `Task` for con
|
|
|
83
81
|
|
|
84
82
|
### Step 0 — [PARALLEL] Analysis Phase
|
|
85
83
|
|
|
86
|
-
Dispatch
|
|
84
|
+
Dispatch three `Task` subagents concurrently:
|
|
87
85
|
|
|
88
86
|
1. **Prompt Validation** — follow `prompt-validator` skill. Validate the user description.
|
|
89
|
-
2. **
|
|
87
|
+
2. **Catalog Analysis** — follow `catalog-analyzer` skill. Read `templates/CATALOG.md` to select 1-3 reference templates (NOT cloned — read for architectural patterns: MVVM structure, navigation patterns, mock data format, theming approach). Then read `templates/shared/MODULES_CATALOG.md` to select shared modules using template context. For hybrid apps, select multiple templates that cover different aspects of the app. **The module list feeds directly into Step 1 (architecture planning).**
|
|
90
88
|
3. **Design Brand Selection** — follow `design-selector` skill. Read `designs/DESIGN_CATALOG.md` and select the best brand.
|
|
91
|
-
4. **Module Selection** — follow `module-selector` skill. Read `templates/shared/MODULES_CATALOG.md` and select which shared modules to include. Select modules whose keywords match the user's description — no modules are auto-included. **The output of this step feeds directly into Step 1 (architecture planning).**
|
|
92
89
|
|
|
93
|
-
Wait for all
|
|
90
|
+
Wait for all three. If prompt validation FAILS: stop immediately.
|
|
94
91
|
|
|
95
92
|
This step also creates the `reports/` directory and initializes `summary.json` with `mode: "scratch"`.
|
|
96
93
|
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.book.core.animation
|
|
2
2
|
|
|
3
|
+
import android.view.HapticFeedbackConstants
|
|
3
4
|
import androidx.compose.animation.*
|
|
4
5
|
import androidx.compose.animation.core.*
|
|
6
|
+
import androidx.compose.foundation.clickable
|
|
7
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
8
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
9
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
5
10
|
import androidx.compose.foundation.layout.offset
|
|
6
11
|
import androidx.compose.runtime.*
|
|
7
12
|
import androidx.compose.ui.Modifier
|
|
8
13
|
import androidx.compose.ui.composed
|
|
9
14
|
import androidx.compose.ui.draw.alpha
|
|
10
15
|
import androidx.compose.ui.draw.scale
|
|
16
|
+
import androidx.compose.ui.draw.shadow
|
|
17
|
+
import androidx.compose.ui.graphics.Color
|
|
18
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
19
|
+
import androidx.compose.ui.platform.LocalView
|
|
11
20
|
import androidx.compose.ui.unit.IntOffset
|
|
21
|
+
import androidx.compose.ui.unit.dp
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Staggered appear animation for list items.
|
|
@@ -118,3 +128,181 @@ fun Modifier.bounceOnChange(
|
|
|
118
128
|
|
|
119
129
|
this.scale(bounceAnim)
|
|
120
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scale-down press feedback for tappable elements.
|
|
134
|
+
* Scales to 0.95 on press with spring return animation.
|
|
135
|
+
*/
|
|
136
|
+
fun Modifier.scaleOnPress(): Modifier = composed {
|
|
137
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
138
|
+
val isPressed by interactionSource.collectIsPressedAsState()
|
|
139
|
+
|
|
140
|
+
val scale by animateFloatAsState(
|
|
141
|
+
targetValue = if (isPressed) 0.95f else 1f,
|
|
142
|
+
animationSpec = spring(
|
|
143
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
144
|
+
stiffness = Spring.StiffnessMedium
|
|
145
|
+
),
|
|
146
|
+
label = "pressScale"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this
|
|
150
|
+
.scale(scale)
|
|
151
|
+
.clickable(
|
|
152
|
+
interactionSource = interactionSource,
|
|
153
|
+
indication = null
|
|
154
|
+
) { }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Triggers haptic feedback on tap.
|
|
159
|
+
*/
|
|
160
|
+
fun Modifier.hapticFeedback(): Modifier = composed {
|
|
161
|
+
val view = LocalView.current
|
|
162
|
+
this.pointerInput(Unit) {
|
|
163
|
+
detectTapGestures {
|
|
164
|
+
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Slide-from-side + fade-in animation on appear.
|
|
171
|
+
*/
|
|
172
|
+
fun Modifier.slideAndFade(
|
|
173
|
+
delayMs: Int = 0,
|
|
174
|
+
durationMs: Int = 400
|
|
175
|
+
): Modifier = composed {
|
|
176
|
+
var isVisible by remember { mutableStateOf(false) }
|
|
177
|
+
|
|
178
|
+
val alpha by animateFloatAsState(
|
|
179
|
+
targetValue = if (isVisible) 1f else 0f,
|
|
180
|
+
animationSpec = tween(
|
|
181
|
+
durationMillis = durationMs,
|
|
182
|
+
delayMillis = delayMs,
|
|
183
|
+
easing = FastOutSlowInEasing
|
|
184
|
+
),
|
|
185
|
+
label = "slideAndFadeAlpha"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
val offsetX by animateIntAsState(
|
|
189
|
+
targetValue = if (isVisible) 0 else 40,
|
|
190
|
+
animationSpec = tween(
|
|
191
|
+
durationMillis = durationMs,
|
|
192
|
+
delayMillis = delayMs,
|
|
193
|
+
easing = FastOutSlowInEasing
|
|
194
|
+
),
|
|
195
|
+
label = "slideAndFadeOffsetX"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
LaunchedEffect(Unit) {
|
|
199
|
+
isVisible = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this
|
|
203
|
+
.alpha(alpha)
|
|
204
|
+
.offset { IntOffset(offsetX, 0) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Subtle colored shadow for cards and elevated surfaces.
|
|
209
|
+
* Uses Material 3 elevation with optional color tinting.
|
|
210
|
+
*/
|
|
211
|
+
fun Modifier.cardShadow(
|
|
212
|
+
color: Color = Color.Black,
|
|
213
|
+
elevation: Float = 4f
|
|
214
|
+
): Modifier = composed {
|
|
215
|
+
this.shadow(
|
|
216
|
+
elevation = elevation.dp,
|
|
217
|
+
ambientColor = color.copy(alpha = 0.08f),
|
|
218
|
+
spotColor = color.copy(alpha = 0.12f),
|
|
219
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Deeper shadow for floating elements (FABs, modals).
|
|
225
|
+
*/
|
|
226
|
+
fun Modifier.elevatedShadow(
|
|
227
|
+
color: Color = Color.Black
|
|
228
|
+
): Modifier = composed {
|
|
229
|
+
this.shadow(
|
|
230
|
+
elevation = 8.dp,
|
|
231
|
+
ambientColor = color.copy(alpha = 0.12f),
|
|
232
|
+
spotColor = color.copy(alpha = 0.16f),
|
|
233
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Shimmer loading overlay for skeleton placeholder views.
|
|
239
|
+
* Sweeps a translucent gradient across the content repeatedly.
|
|
240
|
+
*/
|
|
241
|
+
fun Modifier.shimmer(
|
|
242
|
+
durationMs: Int = 1500
|
|
243
|
+
): Modifier = composed {
|
|
244
|
+
val transition = rememberInfiniteTransition(label = "shimmer")
|
|
245
|
+
val translateAnim by transition.animateFloat(
|
|
246
|
+
initialValue = -1f,
|
|
247
|
+
targetValue = 2f,
|
|
248
|
+
animationSpec = infiniteRepeatable(
|
|
249
|
+
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
|
250
|
+
repeatMode = RepeatMode.Restart
|
|
251
|
+
),
|
|
252
|
+
label = "shimmerTranslate"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
this.alpha(0.3f + 0.7f * ((translateAnim + 1f) / 3f).coerceIn(0f, 1f))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Repeating scale pulse animation for live indicators and notifications.
|
|
260
|
+
*/
|
|
261
|
+
fun Modifier.pulse(
|
|
262
|
+
intensity: Float = 0.05f,
|
|
263
|
+
durationMs: Int = 1000
|
|
264
|
+
): Modifier = composed {
|
|
265
|
+
val transition = rememberInfiniteTransition(label = "pulse")
|
|
266
|
+
val scale by transition.animateFloat(
|
|
267
|
+
initialValue = 1f,
|
|
268
|
+
targetValue = 1f + intensity,
|
|
269
|
+
animationSpec = infiniteRepeatable(
|
|
270
|
+
animation = tween(durationMillis = durationMs, easing = FastOutSlowInEasing),
|
|
271
|
+
repeatMode = RepeatMode.Reverse
|
|
272
|
+
),
|
|
273
|
+
label = "pulseScale"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
this.scale(scale)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Heart/like bounce effect when toggled active.
|
|
281
|
+
* Scales up to 1.3x then springs back. Perfect for favorite buttons.
|
|
282
|
+
*/
|
|
283
|
+
fun Modifier.heartBounce(
|
|
284
|
+
isActive: Boolean
|
|
285
|
+
): Modifier = composed {
|
|
286
|
+
var previousActive by remember { mutableStateOf(isActive) }
|
|
287
|
+
var animatedScale by remember { mutableFloatStateOf(1f) }
|
|
288
|
+
|
|
289
|
+
val bounceAnim by animateFloatAsState(
|
|
290
|
+
targetValue = animatedScale,
|
|
291
|
+
animationSpec = spring(
|
|
292
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
293
|
+
stiffness = Spring.StiffnessMedium
|
|
294
|
+
),
|
|
295
|
+
label = "heartBounce"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
LaunchedEffect(isActive) {
|
|
299
|
+
if (isActive && !previousActive) {
|
|
300
|
+
animatedScale = 1.3f
|
|
301
|
+
kotlinx.coroutines.delay(200)
|
|
302
|
+
animatedScale = 1f
|
|
303
|
+
}
|
|
304
|
+
previousActive = isActive
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.scale(bounceAnim)
|
|
308
|
+
}
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.chat.core.animation
|
|
2
2
|
|
|
3
|
+
import android.view.HapticFeedbackConstants
|
|
3
4
|
import androidx.compose.animation.*
|
|
4
5
|
import androidx.compose.animation.core.*
|
|
6
|
+
import androidx.compose.foundation.clickable
|
|
7
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
8
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
9
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
5
10
|
import androidx.compose.foundation.layout.offset
|
|
6
11
|
import androidx.compose.runtime.*
|
|
7
12
|
import androidx.compose.ui.Modifier
|
|
8
13
|
import androidx.compose.ui.composed
|
|
9
14
|
import androidx.compose.ui.draw.alpha
|
|
10
15
|
import androidx.compose.ui.draw.scale
|
|
16
|
+
import androidx.compose.ui.draw.shadow
|
|
17
|
+
import androidx.compose.ui.graphics.Color
|
|
18
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
19
|
+
import androidx.compose.ui.platform.LocalView
|
|
11
20
|
import androidx.compose.ui.unit.IntOffset
|
|
21
|
+
import androidx.compose.ui.unit.dp
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Staggered appear animation for list items.
|
|
@@ -118,3 +128,181 @@ fun Modifier.bounceOnChange(
|
|
|
118
128
|
|
|
119
129
|
this.scale(bounceAnim)
|
|
120
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scale-down press feedback for tappable elements.
|
|
134
|
+
* Scales to 0.95 on press with spring return animation.
|
|
135
|
+
*/
|
|
136
|
+
fun Modifier.scaleOnPress(): Modifier = composed {
|
|
137
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
138
|
+
val isPressed by interactionSource.collectIsPressedAsState()
|
|
139
|
+
|
|
140
|
+
val scale by animateFloatAsState(
|
|
141
|
+
targetValue = if (isPressed) 0.95f else 1f,
|
|
142
|
+
animationSpec = spring(
|
|
143
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
144
|
+
stiffness = Spring.StiffnessMedium
|
|
145
|
+
),
|
|
146
|
+
label = "pressScale"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this
|
|
150
|
+
.scale(scale)
|
|
151
|
+
.clickable(
|
|
152
|
+
interactionSource = interactionSource,
|
|
153
|
+
indication = null
|
|
154
|
+
) { }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Triggers haptic feedback on tap.
|
|
159
|
+
*/
|
|
160
|
+
fun Modifier.hapticFeedback(): Modifier = composed {
|
|
161
|
+
val view = LocalView.current
|
|
162
|
+
this.pointerInput(Unit) {
|
|
163
|
+
detectTapGestures {
|
|
164
|
+
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Slide-from-side + fade-in animation on appear.
|
|
171
|
+
*/
|
|
172
|
+
fun Modifier.slideAndFade(
|
|
173
|
+
delayMs: Int = 0,
|
|
174
|
+
durationMs: Int = 400
|
|
175
|
+
): Modifier = composed {
|
|
176
|
+
var isVisible by remember { mutableStateOf(false) }
|
|
177
|
+
|
|
178
|
+
val alpha by animateFloatAsState(
|
|
179
|
+
targetValue = if (isVisible) 1f else 0f,
|
|
180
|
+
animationSpec = tween(
|
|
181
|
+
durationMillis = durationMs,
|
|
182
|
+
delayMillis = delayMs,
|
|
183
|
+
easing = FastOutSlowInEasing
|
|
184
|
+
),
|
|
185
|
+
label = "slideAndFadeAlpha"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
val offsetX by animateIntAsState(
|
|
189
|
+
targetValue = if (isVisible) 0 else 40,
|
|
190
|
+
animationSpec = tween(
|
|
191
|
+
durationMillis = durationMs,
|
|
192
|
+
delayMillis = delayMs,
|
|
193
|
+
easing = FastOutSlowInEasing
|
|
194
|
+
),
|
|
195
|
+
label = "slideAndFadeOffsetX"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
LaunchedEffect(Unit) {
|
|
199
|
+
isVisible = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this
|
|
203
|
+
.alpha(alpha)
|
|
204
|
+
.offset { IntOffset(offsetX, 0) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Subtle colored shadow for cards and elevated surfaces.
|
|
209
|
+
* Uses Material 3 elevation with optional color tinting.
|
|
210
|
+
*/
|
|
211
|
+
fun Modifier.cardShadow(
|
|
212
|
+
color: Color = Color.Black,
|
|
213
|
+
elevation: Float = 4f
|
|
214
|
+
): Modifier = composed {
|
|
215
|
+
this.shadow(
|
|
216
|
+
elevation = elevation.dp,
|
|
217
|
+
ambientColor = color.copy(alpha = 0.08f),
|
|
218
|
+
spotColor = color.copy(alpha = 0.12f),
|
|
219
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Deeper shadow for floating elements (FABs, modals).
|
|
225
|
+
*/
|
|
226
|
+
fun Modifier.elevatedShadow(
|
|
227
|
+
color: Color = Color.Black
|
|
228
|
+
): Modifier = composed {
|
|
229
|
+
this.shadow(
|
|
230
|
+
elevation = 8.dp,
|
|
231
|
+
ambientColor = color.copy(alpha = 0.12f),
|
|
232
|
+
spotColor = color.copy(alpha = 0.16f),
|
|
233
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Shimmer loading overlay for skeleton placeholder views.
|
|
239
|
+
* Sweeps a translucent gradient across the content repeatedly.
|
|
240
|
+
*/
|
|
241
|
+
fun Modifier.shimmer(
|
|
242
|
+
durationMs: Int = 1500
|
|
243
|
+
): Modifier = composed {
|
|
244
|
+
val transition = rememberInfiniteTransition(label = "shimmer")
|
|
245
|
+
val translateAnim by transition.animateFloat(
|
|
246
|
+
initialValue = -1f,
|
|
247
|
+
targetValue = 2f,
|
|
248
|
+
animationSpec = infiniteRepeatable(
|
|
249
|
+
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
|
250
|
+
repeatMode = RepeatMode.Restart
|
|
251
|
+
),
|
|
252
|
+
label = "shimmerTranslate"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
this.alpha(0.3f + 0.7f * ((translateAnim + 1f) / 3f).coerceIn(0f, 1f))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Repeating scale pulse animation for live indicators and notifications.
|
|
260
|
+
*/
|
|
261
|
+
fun Modifier.pulse(
|
|
262
|
+
intensity: Float = 0.05f,
|
|
263
|
+
durationMs: Int = 1000
|
|
264
|
+
): Modifier = composed {
|
|
265
|
+
val transition = rememberInfiniteTransition(label = "pulse")
|
|
266
|
+
val scale by transition.animateFloat(
|
|
267
|
+
initialValue = 1f,
|
|
268
|
+
targetValue = 1f + intensity,
|
|
269
|
+
animationSpec = infiniteRepeatable(
|
|
270
|
+
animation = tween(durationMillis = durationMs, easing = FastOutSlowInEasing),
|
|
271
|
+
repeatMode = RepeatMode.Reverse
|
|
272
|
+
),
|
|
273
|
+
label = "pulseScale"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
this.scale(scale)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Heart/like bounce effect when toggled active.
|
|
281
|
+
* Scales up to 1.3x then springs back. Perfect for favorite buttons.
|
|
282
|
+
*/
|
|
283
|
+
fun Modifier.heartBounce(
|
|
284
|
+
isActive: Boolean
|
|
285
|
+
): Modifier = composed {
|
|
286
|
+
var previousActive by remember { mutableStateOf(isActive) }
|
|
287
|
+
var animatedScale by remember { mutableFloatStateOf(1f) }
|
|
288
|
+
|
|
289
|
+
val bounceAnim by animateFloatAsState(
|
|
290
|
+
targetValue = animatedScale,
|
|
291
|
+
animationSpec = spring(
|
|
292
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
293
|
+
stiffness = Spring.StiffnessMedium
|
|
294
|
+
),
|
|
295
|
+
label = "heartBounce"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
LaunchedEffect(isActive) {
|
|
299
|
+
if (isActive && !previousActive) {
|
|
300
|
+
animatedScale = 1.3f
|
|
301
|
+
kotlinx.coroutines.delay(200)
|
|
302
|
+
animatedScale = 1f
|
|
303
|
+
}
|
|
304
|
+
previousActive = isActive
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.scale(bounceAnim)
|
|
308
|
+
}
|
|
@@ -129,7 +129,7 @@ fun ConversationsScreen(onChatClick: (String) -> Unit) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
@Composable
|
|
132
|
-
fun ConversationRow(conversation: Conversation, repository: com.appship.chat.data.DataRepository, onClick: () -> Unit, index: Int) {
|
|
132
|
+
fun ConversationRow(conversation: Conversation, repository: com.appship.chat.data.DataRepository, onClick: () -> Unit, index: Int, modifier: Modifier = Modifier) {
|
|
133
133
|
Row(
|
|
134
134
|
modifier = Modifier
|
|
135
135
|
.fillMaxWidth()
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
package com.appship.dash.core.animation
|
|
2
2
|
|
|
3
|
+
import android.view.HapticFeedbackConstants
|
|
3
4
|
import androidx.compose.animation.*
|
|
4
5
|
import androidx.compose.animation.core.*
|
|
6
|
+
import androidx.compose.foundation.clickable
|
|
7
|
+
import androidx.compose.foundation.gestures.detectTapGestures
|
|
8
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
9
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
5
10
|
import androidx.compose.foundation.layout.offset
|
|
6
11
|
import androidx.compose.runtime.*
|
|
7
12
|
import androidx.compose.ui.Modifier
|
|
8
13
|
import androidx.compose.ui.composed
|
|
9
14
|
import androidx.compose.ui.draw.alpha
|
|
10
15
|
import androidx.compose.ui.draw.scale
|
|
16
|
+
import androidx.compose.ui.draw.shadow
|
|
17
|
+
import androidx.compose.ui.graphics.Color
|
|
18
|
+
import androidx.compose.ui.input.pointer.pointerInput
|
|
19
|
+
import androidx.compose.ui.platform.LocalView
|
|
11
20
|
import androidx.compose.ui.unit.IntOffset
|
|
21
|
+
import androidx.compose.ui.unit.dp
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Staggered appear animation for list items.
|
|
@@ -118,3 +128,181 @@ fun Modifier.bounceOnChange(
|
|
|
118
128
|
|
|
119
129
|
this.scale(bounceAnim)
|
|
120
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scale-down press feedback for tappable elements.
|
|
134
|
+
* Scales to 0.95 on press with spring return animation.
|
|
135
|
+
*/
|
|
136
|
+
fun Modifier.scaleOnPress(): Modifier = composed {
|
|
137
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
138
|
+
val isPressed by interactionSource.collectIsPressedAsState()
|
|
139
|
+
|
|
140
|
+
val scale by animateFloatAsState(
|
|
141
|
+
targetValue = if (isPressed) 0.95f else 1f,
|
|
142
|
+
animationSpec = spring(
|
|
143
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
144
|
+
stiffness = Spring.StiffnessMedium
|
|
145
|
+
),
|
|
146
|
+
label = "pressScale"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this
|
|
150
|
+
.scale(scale)
|
|
151
|
+
.clickable(
|
|
152
|
+
interactionSource = interactionSource,
|
|
153
|
+
indication = null
|
|
154
|
+
) { }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Triggers haptic feedback on tap.
|
|
159
|
+
*/
|
|
160
|
+
fun Modifier.hapticFeedback(): Modifier = composed {
|
|
161
|
+
val view = LocalView.current
|
|
162
|
+
this.pointerInput(Unit) {
|
|
163
|
+
detectTapGestures {
|
|
164
|
+
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Slide-from-side + fade-in animation on appear.
|
|
171
|
+
*/
|
|
172
|
+
fun Modifier.slideAndFade(
|
|
173
|
+
delayMs: Int = 0,
|
|
174
|
+
durationMs: Int = 400
|
|
175
|
+
): Modifier = composed {
|
|
176
|
+
var isVisible by remember { mutableStateOf(false) }
|
|
177
|
+
|
|
178
|
+
val alpha by animateFloatAsState(
|
|
179
|
+
targetValue = if (isVisible) 1f else 0f,
|
|
180
|
+
animationSpec = tween(
|
|
181
|
+
durationMillis = durationMs,
|
|
182
|
+
delayMillis = delayMs,
|
|
183
|
+
easing = FastOutSlowInEasing
|
|
184
|
+
),
|
|
185
|
+
label = "slideAndFadeAlpha"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
val offsetX by animateIntAsState(
|
|
189
|
+
targetValue = if (isVisible) 0 else 40,
|
|
190
|
+
animationSpec = tween(
|
|
191
|
+
durationMillis = durationMs,
|
|
192
|
+
delayMillis = delayMs,
|
|
193
|
+
easing = FastOutSlowInEasing
|
|
194
|
+
),
|
|
195
|
+
label = "slideAndFadeOffsetX"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
LaunchedEffect(Unit) {
|
|
199
|
+
isVisible = true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this
|
|
203
|
+
.alpha(alpha)
|
|
204
|
+
.offset { IntOffset(offsetX, 0) }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Subtle colored shadow for cards and elevated surfaces.
|
|
209
|
+
* Uses Material 3 elevation with optional color tinting.
|
|
210
|
+
*/
|
|
211
|
+
fun Modifier.cardShadow(
|
|
212
|
+
color: Color = Color.Black,
|
|
213
|
+
elevation: Float = 4f
|
|
214
|
+
): Modifier = composed {
|
|
215
|
+
this.shadow(
|
|
216
|
+
elevation = elevation.dp,
|
|
217
|
+
ambientColor = color.copy(alpha = 0.08f),
|
|
218
|
+
spotColor = color.copy(alpha = 0.12f),
|
|
219
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(12.dp)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Deeper shadow for floating elements (FABs, modals).
|
|
225
|
+
*/
|
|
226
|
+
fun Modifier.elevatedShadow(
|
|
227
|
+
color: Color = Color.Black
|
|
228
|
+
): Modifier = composed {
|
|
229
|
+
this.shadow(
|
|
230
|
+
elevation = 8.dp,
|
|
231
|
+
ambientColor = color.copy(alpha = 0.12f),
|
|
232
|
+
spotColor = color.copy(alpha = 0.16f),
|
|
233
|
+
shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp)
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Shimmer loading overlay for skeleton placeholder views.
|
|
239
|
+
* Sweeps a translucent gradient across the content repeatedly.
|
|
240
|
+
*/
|
|
241
|
+
fun Modifier.shimmer(
|
|
242
|
+
durationMs: Int = 1500
|
|
243
|
+
): Modifier = composed {
|
|
244
|
+
val transition = rememberInfiniteTransition(label = "shimmer")
|
|
245
|
+
val translateAnim by transition.animateFloat(
|
|
246
|
+
initialValue = -1f,
|
|
247
|
+
targetValue = 2f,
|
|
248
|
+
animationSpec = infiniteRepeatable(
|
|
249
|
+
animation = tween(durationMillis = durationMs, easing = LinearEasing),
|
|
250
|
+
repeatMode = RepeatMode.Restart
|
|
251
|
+
),
|
|
252
|
+
label = "shimmerTranslate"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
this.alpha(0.3f + 0.7f * ((translateAnim + 1f) / 3f).coerceIn(0f, 1f))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Repeating scale pulse animation for live indicators and notifications.
|
|
260
|
+
*/
|
|
261
|
+
fun Modifier.pulse(
|
|
262
|
+
intensity: Float = 0.05f,
|
|
263
|
+
durationMs: Int = 1000
|
|
264
|
+
): Modifier = composed {
|
|
265
|
+
val transition = rememberInfiniteTransition(label = "pulse")
|
|
266
|
+
val scale by transition.animateFloat(
|
|
267
|
+
initialValue = 1f,
|
|
268
|
+
targetValue = 1f + intensity,
|
|
269
|
+
animationSpec = infiniteRepeatable(
|
|
270
|
+
animation = tween(durationMillis = durationMs, easing = FastOutSlowInEasing),
|
|
271
|
+
repeatMode = RepeatMode.Reverse
|
|
272
|
+
),
|
|
273
|
+
label = "pulseScale"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
this.scale(scale)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Heart/like bounce effect when toggled active.
|
|
281
|
+
* Scales up to 1.3x then springs back. Perfect for favorite buttons.
|
|
282
|
+
*/
|
|
283
|
+
fun Modifier.heartBounce(
|
|
284
|
+
isActive: Boolean
|
|
285
|
+
): Modifier = composed {
|
|
286
|
+
var previousActive by remember { mutableStateOf(isActive) }
|
|
287
|
+
var animatedScale by remember { mutableFloatStateOf(1f) }
|
|
288
|
+
|
|
289
|
+
val bounceAnim by animateFloatAsState(
|
|
290
|
+
targetValue = animatedScale,
|
|
291
|
+
animationSpec = spring(
|
|
292
|
+
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
293
|
+
stiffness = Spring.StiffnessMedium
|
|
294
|
+
),
|
|
295
|
+
label = "heartBounce"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
LaunchedEffect(isActive) {
|
|
299
|
+
if (isActive && !previousActive) {
|
|
300
|
+
animatedScale = 1.3f
|
|
301
|
+
kotlinx.coroutines.delay(200)
|
|
302
|
+
animatedScale = 1f
|
|
303
|
+
}
|
|
304
|
+
previousActive = isActive
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.scale(bounceAnim)
|
|
308
|
+
}
|
|
@@ -15,6 +15,7 @@ import com.appship.dash.features.alerts.AlertsScreen
|
|
|
15
15
|
import com.appship.dash.features.dashboard.DashboardScreen
|
|
16
16
|
import com.appship.dash.features.settings.SettingsScreen
|
|
17
17
|
|
|
18
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
18
19
|
@Composable
|
|
19
20
|
fun MainScreen() {
|
|
20
21
|
var selectedTab by remember { mutableStateOf(0) }
|