@mediacubeco/expo-template-fsd 2.3.0 → 2.4.0
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/commands/git-commit.md +164 -0
- package/.claude/rules/architecture.md +38 -0
- package/.claude/rules/{prettier-eslint-typescript.mdc → prettier-eslint-typescript.md} +5 -10
- package/.claude/settings.json +2 -2
- package/.claude/skills/api/SKILL.md +362 -0
- package/.claude/skills/api/references/client/overview.md +106 -0
- package/.claude/skills/api/references/hooks/index.ts +3 -0
- package/.claude/skills/api/references/hooks/use-api-health.ts +25 -0
- package/.claude/skills/api/references/hooks/use-api-post.ts +46 -0
- package/.claude/skills/api/references/hooks/use-api-posts.ts +24 -0
- package/.claude/skills/api/references/mock/post.mock.ts +29 -0
- package/.claude/skills/{generate-api-model/references → api/references/model}/entities/common.ts +5 -2
- package/.claude/skills/api/references/model/resources/common.ts +9 -0
- package/.claude/skills/api/references/queries/health.ts +14 -0
- package/.claude/skills/api/references/queries/index.ts +2 -0
- package/.claude/skills/api/references/queries/post.ts +23 -0
- package/.claude/skills/api/references/repository/health/health.ts +11 -0
- package/.claude/skills/api/references/repository/health/health.types.ts +25 -0
- package/.claude/skills/api/references/repository/health/index.ts +2 -0
- package/.claude/skills/api/references/repository/post/index.ts +9 -0
- package/.claude/skills/api/references/repository/post/post.ts +34 -0
- package/.claude/skills/api/references/repository/post/post.types.ts +61 -0
- package/.claude/skills/code-style/SKILL.md +1025 -1
- package/.claude/skills/code-style/references/components/payout-speed-option/index.ts +2 -0
- package/.claude/skills/code-style/references/components/payout-speed-option/payout-speed-option.styles.ts +76 -0
- package/.claude/skills/code-style/references/components/payout-speed-option/payout-speed-option.tsx +58 -0
- package/.claude/skills/code-style/references/components/payout-speed-option/payout-speed-option.types.ts +22 -0
- package/.claude/skills/code-style/references/components/post/index.ts +2 -0
- package/.claude/skills/code-style/references/components/post/post.styles.ts +19 -0
- package/.claude/skills/code-style/references/components/post/post.tsx +21 -0
- package/.claude/skills/code-style/references/components/post/post.types.ts +9 -0
- package/.claude/skills/code-style/references/components/wrap/index.ts +2 -0
- package/.claude/skills/code-style/references/components/wrap/wrap.styles.ts +15 -0
- package/.claude/skills/code-style/references/components/wrap/wrap.tsx +31 -0
- package/.claude/skills/code-style/references/components/wrap/wrap.types.ts +10 -0
- package/.claude/skills/code-style/references/hooks/use-posts.ts +37 -0
- package/.claude/skills/code-style/references/pages/post/index.ts +4 -0
- package/.claude/skills/code-style/references/pages/post/post-details/index.ts +1 -0
- package/.claude/skills/code-style/references/pages/post/post-details/post-details.tsx +13 -0
- package/.claude/skills/code-style/references/pages/post/post-guide/index.ts +1 -0
- package/.claude/skills/code-style/references/pages/post/post-guide/post-guide.tsx +17 -0
- package/.claude/skills/code-style/references/pages/post/post-list/index.ts +1 -0
- package/.claude/skills/code-style/references/pages/post/post-list/post-list.styles.ts +17 -0
- package/.claude/skills/code-style/references/pages/post/post-list/post-list.tsx +25 -0
- package/.claude/skills/code-style/references/pages/post/post-share/index.ts +1 -0
- package/.claude/skills/code-style/references/pages/post/post-share/post-share.tsx +13 -0
- package/.claude/skills/code-style/references/routes/(stack)/_layout.tsx +18 -0
- package/.claude/skills/code-style/references/routes/(stack)/details.tsx +8 -0
- package/.claude/skills/code-style/references/routes/(stack)/guide.tsx +8 -0
- package/.claude/skills/code-style/references/routes/(stack)/index.tsx +8 -0
- package/.claude/skills/code-style/references/routes/(stack)/share.tsx +8 -0
- package/.claude/skills/code-style/references/routes/(tabs)/(post-details)/index.tsx +8 -0
- package/.claude/skills/code-style/references/routes/(tabs)/(post-list)/index.tsx +8 -0
- package/.claude/skills/code-style/references/routes/(tabs)/_layout.tsx +50 -0
- package/.claude/skills/code-style/references/routes/_app.tsx +20 -0
- package/.claude/skills/code-style/references/routes/guide.tsx +8 -0
- package/.claude/skills/code-style/references/routes/share.tsx +8 -0
- package/.claude/skills/code-style/references/store/use-post-draft-store.ts +62 -0
- package/.claude/skills/design-style/SKILL.md +320 -0
- package/.env +3 -1
- package/.env.example +3 -1
- package/CLAUDE.md +5 -11
- package/app.json +6 -3
- package/axios.d.ts +5 -0
- package/env.d.ts +3 -1
- package/package.json +33 -40
- package/patches/react-native-screens+4.25.2.patch +118 -0
- package/react-query.d.ts +13 -0
- package/src/app/(root)/(home)/_layout.tsx +2 -2
- package/src/app/(root)/(home)/index.tsx +2 -2
- package/src/app/(root)/(settings)/_layout.tsx +2 -2
- package/src/app/(root)/(settings)/index.tsx +2 -2
- package/src/app/(root)/_layout.tsx +35 -32
- package/src/pages/home/home-main.tsx +3 -7
- package/src/pages/settings/settings-main.tsx +3 -7
- package/src/shared/assets/fonts/index.ts +3 -3
- package/src/shared/assets/fonts/sf-pro-text-bold.ttf +0 -0
- package/src/shared/assets/fonts/sf-pro-text-regular.ttf +0 -0
- package/src/shared/assets/fonts/sf-pro-text-semibold.ttf +0 -0
- package/src/shared/assets/icons/back.tsx +21 -0
- package/src/shared/assets/icons/close.tsx +21 -0
- package/src/shared/assets/icons/common.ts +8 -0
- package/src/shared/assets/icons/default.tsx +44 -0
- package/src/shared/assets/icons/index.ts +13 -1
- package/src/shared/assets/icons/refresh.tsx +23 -0
- package/src/shared/assets/icons/share.tsx +38 -0
- package/src/shared/assets/icons/type.ts +7 -0
- package/src/shared/assets/translations/index.ts +4 -4
- package/src/shared/constants/config.ts +13 -0
- package/src/shared/constants/environment.ts +16 -4
- package/src/shared/constants/index.ts +1 -0
- package/src/shared/hooks/index.ts +1 -4
- package/src/shared/hooks/use-compose.ts +23 -0
- package/src/shared/lib/api/api.client.ts +157 -0
- package/src/shared/lib/api/api.provider.tsx +14 -36
- package/src/shared/lib/api/api.ts +1 -5
- package/src/shared/lib/api/api.types.ts +6 -3
- package/src/shared/lib/api/hooks/index.ts +1 -0
- package/src/shared/lib/api/index.ts +2 -3
- package/src/shared/lib/api/models/api-instance/api-instance.client.ts +50 -0
- package/src/shared/lib/api/models/api-instance/api-instance.ts +30 -82
- package/src/shared/lib/api/models/api-instance/api-instance.types.ts +16 -18
- package/src/shared/lib/api/models/api-instance/api-instance.utils.ts +13 -20
- package/src/shared/lib/api/models/api-instance/index.ts +3 -0
- package/src/shared/lib/api/queries/index.ts +1 -0
- package/src/shared/lib/api/repositories/index.ts +1 -1
- package/src/shared/lib/localization/constants/index.ts +16 -5
- package/src/shared/lib/localization/index.ts +5 -1
- package/src/shared/lib/navigation/hooks/index.ts +8 -3
- package/src/shared/{hooks/use-breakpoints.ts → lib/navigation/hooks/use-navigation-breakpoints.ts} +2 -5
- package/src/shared/lib/navigation/hooks/use-navigation-dimensions.ts +12 -0
- package/src/shared/lib/navigation/hooks/use-navigation-options.tsx +175 -0
- package/src/shared/lib/navigation/hooks/{use-navigation.ts → use-navigation-route.ts} +2 -2
- package/src/shared/lib/navigation/hooks/use-navigation-safe-area.ts +61 -0
- package/src/shared/lib/navigation/hooks/use-navigation-tabs-native.ts +10 -0
- package/src/shared/lib/navigation/hooks/use-navigation-tabs.tsx +10 -0
- package/src/shared/lib/navigation/hooks/use-navigation-transition.ts +27 -0
- package/src/shared/lib/navigation/index.ts +11 -4
- package/src/shared/lib/navigation/navigation-tabs-native.context.tsx +5 -0
- package/src/shared/lib/navigation/navigation-tabs-native.provider.tsx +17 -0
- package/src/shared/lib/navigation/navigation-tabs.context.tsx +10 -0
- package/src/shared/lib/navigation/navigation-tabs.provider.tsx +22 -0
- package/src/shared/lib/navigation/navigation.provider.tsx +6 -1
- package/src/shared/lib/storage/index.ts +1 -1
- package/src/shared/lib/storage/storage.provider.tsx +4 -0
- package/src/shared/lib/storage/storage.ts +29 -25
- package/src/shared/lib/theme/constants/animations.ts +26 -0
- package/src/shared/lib/theme/constants/colors.ts +336 -43
- package/src/shared/lib/theme/constants/delays.ts +1 -1
- package/src/shared/lib/theme/constants/durations.ts +1 -1
- package/src/shared/lib/theme/constants/fonts.ts +2 -1
- package/src/shared/lib/theme/constants/index.ts +42 -20
- package/src/shared/lib/theme/constants/shadows.ts +7 -25
- package/src/shared/lib/theme/constants/sizes.ts +11 -2
- package/src/shared/lib/theme/constants/transparencies.ts +1 -1
- package/src/shared/lib/theme/hooks/use-theme-setup.ts +3 -1
- package/src/shared/lib/theme/index.ts +10 -1
- package/src/shared/lib/theme/theme.provider.tsx +45 -19
- package/src/shared/lib/theme/theme.scope.tsx +43 -0
- package/src/shared/lib/theme/theme.types.ts +15 -3
- package/src/shared/lib/theme/utils/color.ts +81 -0
- package/src/shared/lib/theme/utils/index.ts +2 -0
- package/src/shared/lib/theme/utils/snapshot.ts +17 -0
- package/src/shared/lib/theme/utils/storage.ts +5 -2
- package/src/shared/services/browser/browser.ts +66 -0
- package/src/shared/services/browser/index.ts +1 -0
- package/src/shared/services/cookies/cookies.ts +73 -0
- package/src/shared/services/cookies/index.ts +1 -0
- package/src/shared/services/index.ts +5 -0
- package/src/shared/services/linking/index.ts +1 -0
- package/src/shared/services/linking/linking.ts +22 -0
- package/src/shared/services/share/index.ts +1 -0
- package/src/shared/services/share/share.ts +28 -0
- package/src/shared/services/universal-links/index.ts +104 -0
- package/src/shared/services/universal-links/universal-links.ts +25 -0
- package/src/shared/services/universal-links/universal-links.types.ts +5 -0
- package/src/shared/ui/atoms/blur/blur.tsx +9 -4
- package/src/shared/ui/atoms/blur/blur.types.ts +1 -1
- package/src/shared/ui/atoms/blur-progress/blur-progress.styles.ts +16 -0
- package/src/shared/ui/atoms/blur-progress/blur-progress.tsx +30 -0
- package/src/shared/ui/atoms/blur-progress/blur-progress.types.tsx +5 -0
- package/src/shared/ui/atoms/blur-progress/index.ts +1 -0
- package/src/shared/ui/atoms/glass/glass.tsx +13 -5
- package/src/shared/ui/atoms/glass/glass.types.ts +1 -1
- package/src/shared/ui/atoms/icon/icon.styles.ts +82 -0
- package/src/shared/ui/atoms/icon/icon.tsx +21 -0
- package/src/shared/ui/atoms/icon/icon.types.ts +23 -0
- package/src/shared/ui/atoms/icon/index.ts +2 -0
- package/src/shared/ui/atoms/index.ts +7 -0
- package/src/shared/ui/atoms/skeleton/index.ts +2 -0
- package/src/shared/ui/atoms/skeleton/skeleton.styles.ts +69 -0
- package/src/shared/ui/atoms/skeleton/skeleton.tsx +26 -0
- package/src/shared/ui/atoms/skeleton/skeleton.types.ts +9 -0
- package/src/shared/ui/atoms/spinner/index.ts +2 -0
- package/src/shared/ui/atoms/spinner/spinner.styles.ts +102 -0
- package/src/shared/ui/atoms/spinner/spinner.tsx +48 -0
- package/src/shared/ui/atoms/spinner/spinner.types.ts +25 -0
- package/src/shared/ui/atoms/text/index.ts +2 -0
- package/src/shared/ui/atoms/text/text.styles.ts +186 -0
- package/src/shared/ui/atoms/text/text.tsx +37 -0
- package/src/shared/ui/atoms/text/text.types.ts +35 -0
- package/src/shared/ui/atoms/touch/components/haptics/haptics.styles.ts +21 -0
- package/src/shared/ui/atoms/touch/components/haptics/haptics.tsx +28 -0
- package/src/shared/ui/atoms/touch/components/haptics/haptics.types.ts +17 -0
- package/src/shared/ui/atoms/touch/components/haptics/index.ts +2 -0
- package/src/shared/ui/atoms/touch/components/index.ts +3 -0
- package/src/shared/ui/atoms/touch/components/ripple/index.ts +2 -0
- package/src/shared/ui/atoms/touch/components/ripple/ripple.styles.ts +78 -0
- package/src/shared/ui/atoms/touch/components/ripple/ripple.tsx +29 -0
- package/src/shared/ui/atoms/touch/components/ripple/ripple.types.ts +17 -0
- package/src/shared/ui/atoms/touch/components/spring/index.ts +2 -0
- package/src/shared/ui/atoms/touch/components/spring/spring.styles.ts +31 -0
- package/src/shared/ui/atoms/touch/components/spring/spring.tsx +28 -0
- package/src/shared/ui/atoms/touch/components/spring/spring.types.ts +17 -0
- package/src/shared/ui/atoms/touch/index.ts +2 -0
- package/src/shared/ui/atoms/touch/touch.styles.ts +14 -0
- package/src/shared/ui/atoms/touch/touch.tsx +137 -0
- package/src/shared/ui/atoms/touch/touch.types.ts +27 -0
- package/src/shared/ui/atoms/visibility/index.ts +2 -0
- package/src/shared/ui/atoms/visibility/visibility.styles.ts +85 -0
- package/src/shared/ui/atoms/visibility/visibility.tsx +61 -0
- package/src/shared/ui/atoms/visibility/visibility.types.ts +34 -0
- package/src/shared/ui/molecules/keyboard-area/keyboard-area.styles.ts +11 -9
- package/src/shared/ui/molecules/keyboard-area/keyboard-area.tsx +3 -6
- package/src/shared/ui/molecules/refresh-control/refresh-control.tsx +3 -5
- package/src/shared/ui/molecules/refresh-control/refresh-control.types.ts +1 -1
- package/src/shared/ui/molecules/slider/components/indicator/components/dot/dot.styles.ts +41 -0
- package/src/shared/ui/molecules/slider/components/indicator/components/dot/dot.tsx +12 -0
- package/src/shared/ui/molecules/slider/components/indicator/components/dot/dot.types.ts +13 -0
- package/src/shared/ui/molecules/slider/components/indicator/components/dot/index.ts +2 -0
- package/src/shared/ui/molecules/slider/components/indicator/index.ts +2 -0
- package/src/shared/ui/molecules/slider/components/indicator/indicator.styles.ts +20 -0
- package/src/shared/ui/molecules/slider/components/indicator/indicator.tsx +22 -0
- package/src/shared/ui/molecules/slider/components/indicator/indicator.types.ts +10 -0
- package/src/shared/ui/molecules/slider/index.ts +3 -0
- package/src/shared/ui/molecules/slider/slider.styles.ts +68 -0
- package/src/shared/ui/molecules/slider/slider.tsx +64 -0
- package/src/shared/ui/molecules/slider/slider.types.ts +28 -0
- package/src/shared/ui/organisms/bar/bar.styles.ts +100 -0
- package/src/shared/ui/organisms/bar/bar.tsx +89 -0
- package/src/shared/ui/organisms/bar/bar.types.ts +49 -0
- package/src/shared/ui/organisms/bar/index.ts +2 -0
- package/src/shared/ui/organisms/bar-tab/bar-tab.tsx +9 -0
- package/src/shared/ui/organisms/bar-tab/bar-tab.types.ts +5 -0
- package/src/shared/ui/organisms/bar-tab/components/index.ts +1 -0
- package/src/shared/ui/organisms/bar-tab/components/tab/index.ts +2 -0
- package/src/shared/ui/organisms/bar-tab/components/tab/tab.styles.ts +21 -0
- package/src/shared/ui/organisms/bar-tab/components/tab/tab.tsx +18 -0
- package/src/shared/ui/organisms/bar-tab/components/tab/tab.types.ts +5 -0
- package/src/shared/ui/organisms/bar-tab/index.ts +2 -0
- package/src/shared/ui/organisms/bar-tab-navigation/bar-tab-navigation.styles.ts +22 -0
- package/src/shared/ui/organisms/bar-tab-navigation/bar-tab-navigation.tsx +66 -0
- package/src/shared/ui/organisms/bar-tab-navigation/bar-tab-navigation.types.ts +5 -0
- package/src/shared/ui/organisms/bar-tab-navigation/components/index.ts +1 -0
- package/src/shared/ui/organisms/bar-tab-navigation/components/tab-navigation/index.ts +2 -0
- package/src/shared/ui/organisms/bar-tab-navigation/components/tab-navigation/tab-navigation.styles.ts +56 -0
- package/src/shared/ui/organisms/bar-tab-navigation/components/tab-navigation/tab-navigation.tsx +30 -0
- package/src/shared/ui/organisms/bar-tab-navigation/components/tab-navigation/tab-navigation.types.ts +7 -0
- package/src/shared/ui/organisms/bar-tab-navigation/index.ts +2 -0
- package/src/shared/ui/organisms/index.ts +4 -1
- package/src/shared/ui/organisms/slider/components/indicator/components/dot/dot.styles.ts +41 -0
- package/src/shared/ui/organisms/slider/components/indicator/components/dot/dot.tsx +12 -0
- package/src/shared/ui/organisms/slider/components/indicator/components/dot/dot.types.ts +13 -0
- package/src/shared/ui/organisms/slider/components/indicator/components/dot/index.ts +2 -0
- package/src/shared/ui/organisms/slider/components/indicator/index.ts +2 -0
- package/src/shared/ui/organisms/slider/components/indicator/indicator.styles.ts +20 -0
- package/src/shared/ui/organisms/slider/components/indicator/indicator.tsx +22 -0
- package/src/shared/ui/organisms/slider/components/indicator/indicator.types.ts +10 -0
- package/src/shared/ui/organisms/slider/index.ts +3 -0
- package/src/shared/ui/organisms/slider/slider.styles.ts +68 -0
- package/src/shared/ui/organisms/slider/slider.tsx +64 -0
- package/src/shared/ui/organisms/slider/slider.types.ts +28 -0
- package/src/shared/ui/templates/index.ts +3 -0
- package/src/shared/ui/templates/modal-container/index.ts +13 -0
- package/src/shared/ui/templates/modal-container/modal-container-list.tsx +45 -0
- package/src/shared/ui/templates/modal-container/modal-container-scroll.tsx +66 -0
- package/src/shared/ui/templates/modal-container/modal-container-section-list.tsx +46 -0
- package/src/shared/ui/templates/modal-container/modal-container.hooks.tsx +172 -0
- package/src/shared/ui/templates/modal-container/modal-container.styles.ts +121 -0
- package/src/shared/ui/templates/modal-container/modal-container.types.ts +69 -0
- package/src/shared/ui/templates/screen-container/index.ts +1 -1
- package/src/shared/ui/templates/screen-container/screen-container-list.tsx +28 -10
- package/src/shared/ui/templates/screen-container/screen-container-scroll.tsx +28 -10
- package/src/shared/ui/templates/screen-container/screen-container-section-list.tsx +29 -11
- package/src/shared/ui/templates/screen-container/screen-container.hooks.tsx +79 -8
- package/src/shared/ui/templates/screen-container/screen-container.styles.ts +105 -15
- package/src/shared/ui/templates/screen-container/screen-container.types.tsx +34 -7
- package/src/shared/ui/templates/screen-header/components/index.ts +6 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-description/index.ts +2 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-description/screen-header-description.tsx +15 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-description/screen-header-description.types.ts +11 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon/index.ts +2 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon/screen-header-icon.styles.ts +44 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon/screen-header-icon.tsx +65 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon/screen-header-icon.types.ts +14 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon-back/index.ts +2 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon-back/screen-header-icon-back.tsx +17 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon-back/screen-header-icon-back.types.ts +6 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon-close/index.ts +2 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon-close/screen-header-icon-close.tsx +27 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-icon-close/screen-header-icon-close.types.ts +10 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-info/index.ts +2 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-info/screen-header-info.tsx +15 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-info/screen-header-info.types.ts +10 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-title/index.ts +2 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-title/screen-header-title.tsx +19 -0
- package/src/shared/ui/templates/screen-header/components/screen-header-title/screen-header-title.types.ts +13 -0
- package/src/shared/ui/templates/screen-header/index.ts +3 -0
- package/src/shared/ui/templates/screen-header/screen-header.styles.ts +49 -0
- package/src/shared/ui/templates/screen-header/screen-header.tsx +26 -0
- package/src/shared/ui/templates/screen-header/screen-header.types.ts +15 -0
- package/src/shared/ui/templates/web-container/index.ts +2 -0
- package/src/shared/ui/templates/web-container/web-container.styles.ts +34 -0
- package/src/shared/ui/templates/web-container/web-container.tsx +172 -0
- package/src/shared/ui/templates/web-container/web-container.types.ts +21 -0
- package/.claude/rules/app-routing.mdc +0 -47
- package/.claude/rules/exports-at-end.mdc +0 -55
- package/.claude/rules/kebab-case-naming.mdc +0 -29
- package/.claude/rules/mediacubeco-packages.mdc +0 -56
- package/.claude/skills/generate-api-model/SKILL.md +0 -230
- package/.claude/skills/generate-api-model/references/resources/common.ts +0 -8
- package/.claude/skills/generate-api-model-mock/SKILL.md +0 -56
- package/.claude/skills/git-commit/SKILL.md +0 -139
- package/.gitignore +0 -34
- package/src/shared/assets/icons/like.tsx +0 -17
- package/src/shared/hooks/use-animated.ts +0 -200
- package/src/shared/hooks/use-mixins.ts +0 -36
- package/src/shared/hooks/use-safe-area.ts +0 -75
- package/src/shared/lib/api/repositories/auth/auth.ts +0 -23
- package/src/shared/lib/api/repositories/auth/auth.types.ts +0 -22
- package/src/shared/lib/api/repositories/auth/index.ts +0 -1
- package/src/shared/lib/navigation/hooks/use-navigation-native-tabs.ts +0 -10
- package/src/shared/lib/navigation/hooks/use-navigation-options.ts +0 -123
- package/src/shared/lib/navigation/navigation-native-tabs.context.tsx +0 -5
- package/src/shared/lib/navigation/navigation-native-tabs.provider.tsx +0 -12
- package/src/shared/lib/theme/constants/borders.ts +0 -8
- package/src/shared/lib/theme/constants/weights.ts +0 -13
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/index.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/post/index.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/post/post-comment.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/post/post.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/user/index.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/user/user-settings.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/user/user-verification.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/entities/user/user.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/index.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/post/index.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/post/post-comment.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/post/post.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/user/index.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/user/user-settings.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/user/user-verification.ts +0 -0
- /package/.claude/skills/{generate-api-model/references → api/references/model}/resources/user/user.ts +0 -0
|
@@ -1 +1,1025 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: code-style
|
|
3
|
+
description: >-
|
|
4
|
+
Project code style rules: naming (List/Count suffixes, boolean prefixes,
|
|
5
|
+
plural dictionaries), exports at end of file, mandatory use of @mediacubeco
|
|
6
|
+
utilities, useXxxStyles pattern, the visual token map (COLORS roles and
|
|
7
|
+
alpha steps, SIZES spacing rhythm and computed radii, the Text typography
|
|
8
|
+
scale, Glass/Blur materials, ANIMATIONS timing/spring tokens, Touch press
|
|
9
|
+
feedback props withRipple / withSpring / withHaptics, the selection-option
|
|
10
|
+
scaffold), custom React hooks (use-* files, facade hooks), memoization
|
|
11
|
+
policy, JSX prop conventions, thin route files in src/app (Expo Router, Page
|
|
12
|
+
components, sheet/web options), the keep-src/app-empty rule, Zustand client
|
|
13
|
+
state (stores, persist, selectors), the folder-per-page scaffold in
|
|
14
|
+
src/pages, mandatory pull-to-refresh (onRefresh) on screens with server
|
|
15
|
+
data, keyboard UX for multi-input forms (useFormFields), the translations
|
|
16
|
+
workflow (source of truth in ../front/lang, generated assets), and the
|
|
17
|
+
scaffold for shared UI components in src/shared/ui (atoms, molecules,
|
|
18
|
+
organisms, templates).
|
|
19
|
+
Use this skill EVERY time TypeScript/TSX code is written, edited, refactored,
|
|
20
|
+
or reviewed in this project — new components, hooks, API layers, pages, bug
|
|
21
|
+
fixes, and small edits alike. Also use when the user asks to create a
|
|
22
|
+
component or UI element, to add a translation key, to build or edit a form,
|
|
23
|
+
to add a Zustand store, or to check or fix code style, naming, or
|
|
24
|
+
consistency.
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Code Style
|
|
28
|
+
|
|
29
|
+
These rules describe how code is written in this project. They exist so that any
|
|
30
|
+
file reads as if one person wrote it. When editing existing code, follow these
|
|
31
|
+
rules for the code you touch; older code may deviate — the rules win.
|
|
32
|
+
|
|
33
|
+
## Naming
|
|
34
|
+
|
|
35
|
+
### Variables
|
|
36
|
+
|
|
37
|
+
- **Arrays always end with `List`** — variables, props, entity fields, function
|
|
38
|
+
return names. The suffix tells the reader "iterable" without looking at the type.
|
|
39
|
+
- ✅ `countryList`, `dataList: string[]`, `getCountryList()`, `snapToOffsetList`
|
|
40
|
+
- ❌ `countries`, `data: string[]`, `items`
|
|
41
|
+
- Exception: a generic list component whose element type is the generic
|
|
42
|
+
parameter keeps the platform-conventional `data: Item[]` prop (matching
|
|
43
|
+
RN's `FlatList`-style API).
|
|
44
|
+
- **Booleans start with `is` / `can` / `did` / `has` / `should` / `with`** —
|
|
45
|
+
everywhere: variables, props, fields. `isPending`, `isDisabled`,
|
|
46
|
+
`didFinalized`, `canSubmit`, `withSpring`, `withHaptics`, `withAvatar`.
|
|
47
|
+
- Exception: controlled selection components (`Switch`, `Checkbox`, `Radio`)
|
|
48
|
+
keep the platform-conventional `value?: boolean` prop.
|
|
49
|
+
- **Dictionary/Record objects are plural** — objects whose keys map to a set of
|
|
50
|
+
same-shaped values: `QUERY_KEYS`, `COLORS`, `BREAKPOINTS`, `TRANSPARENCIES`.
|
|
51
|
+
- **Counters use the `Count` suffix**: `tabCount`, `itemCount`
|
|
52
|
+
(not `numberOfTabs`, not `length` copies without a name).
|
|
53
|
+
- **Design constants are UPPER_SNAKE_CASE even when computed inside a hook.**
|
|
54
|
+
Anything that plays the role of a layout/design token — sizes, paddings,
|
|
55
|
+
widths — keeps the constant casing regardless of where it is derived:
|
|
56
|
+
`TAB_WIDTH`, `TABS_PADDING`, `SLIDER_ITEM_WIDTH`.
|
|
57
|
+
|
|
58
|
+
### Functions and props
|
|
59
|
+
|
|
60
|
+
- Internal handlers: `handleX` (`handlePress`, `handleWrapperLayout`).
|
|
61
|
+
- Callback props: `onX` (`onPress`, `onChange`). A component receives `onX`,
|
|
62
|
+
wraps it in `handleX`, and passes `handleX` down.
|
|
63
|
+
- A prop that shadows an internal name gets the `init` prefix when destructured:
|
|
64
|
+
`header: initHeader`, `renderItem: initRenderItem`.
|
|
65
|
+
|
|
66
|
+
### Types
|
|
67
|
+
|
|
68
|
+
- Object shapes are described with type aliases only — no `interface`.
|
|
69
|
+
- **`enum` is welcome** for a fixed set of related constants (theme modes,
|
|
70
|
+
locale codes, transport methods): string values, `PascalCase` name,
|
|
71
|
+
`PascalCase` members, Russian JSDoc like any other top-level declaration.
|
|
72
|
+
A union type is for ad-hoc unions of literals/types that don't form a
|
|
73
|
+
named constant set — don't degrade an `enum` into a union.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
/** Способ доставки уведомления, выбранный пользователем. */
|
|
77
|
+
enum NotificationChannel {
|
|
78
|
+
Push = 'push',
|
|
79
|
+
Email = 'email',
|
|
80
|
+
Sms = 'sms',
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
- **A constant lookup map is typed by `as const`, never by an explicit
|
|
84
|
+
`Record<…>` annotation.** Appending `as const` preserves the literal value
|
|
85
|
+
types and makes the map `readonly`; a `Record<K, V>` annotation widens every
|
|
86
|
+
value to `V` (losing the literals) and forces you to restate the key set by
|
|
87
|
+
hand. Let TypeScript infer — the object _is_ the type.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// ❌ explicit Record annotation — widens values, restates keys
|
|
91
|
+
const VALUE_BY_KEY: Record<Key, Value> = {
|
|
92
|
+
alpha: 'first',
|
|
93
|
+
beta: 'second',
|
|
94
|
+
gamma: 'third',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ✅ as const — literal values, inferred keys, readonly
|
|
98
|
+
const VALUE_BY_KEY = {
|
|
99
|
+
alpha: 'first',
|
|
100
|
+
beta: 'second',
|
|
101
|
+
gamma: 'third',
|
|
102
|
+
} as const
|
|
103
|
+
```
|
|
104
|
+
- Component props: `<Name>Props`. Styles-hook context: `<Name>StylesContext`
|
|
105
|
+
(plural `Styles`, matching the hook name).
|
|
106
|
+
- Refs are passed via a plain `ref?: Ref<T>` prop (React 19 treats `ref` as a
|
|
107
|
+
regular prop) — never `forwardRef` (it is deprecated). The component forwards
|
|
108
|
+
`ref` to the most useful inner element.
|
|
109
|
+
|
|
110
|
+
### Files
|
|
111
|
+
|
|
112
|
+
- All files and folders are **kebab-case**: only lowercase letters, digits, and
|
|
113
|
+
hyphens (`some-name.ts`). No camelCase, PascalCase, or snake_case. Dots only
|
|
114
|
+
for extensions and config modifiers (`eslint.config.js`).
|
|
115
|
+
- Hooks files start with `use-` (`use-breakpoints.ts`).
|
|
116
|
+
- A shared UI component is a 4-file folder — see "Shared UI components" below.
|
|
117
|
+
|
|
118
|
+
## Imports and exports
|
|
119
|
+
|
|
120
|
+
- **All exports live at the very end of the file**, after imports, types, and
|
|
121
|
+
implementation. Never scatter `export` on declarations (`export const`,
|
|
122
|
+
`export function`, `export class`) — declare first, then export in one block
|
|
123
|
+
at the bottom: it makes the file's public API readable at a glance.
|
|
124
|
+
- Order at the bottom: `export { … }` → `export type { … }` →
|
|
125
|
+
`export default …` last.
|
|
126
|
+
- `export default` only for React components (and Expo Router pages).
|
|
127
|
+
- Type imports are inline: `import { type TextProps } from './text.types'`.
|
|
128
|
+
- Barrel `index.ts` files contain only re-exports (`export … from`), and the
|
|
129
|
+
lines are sorted **alphabetically by module path**, top to bottom (A→Z / а→я).
|
|
130
|
+
There is no "newest goes last" — adding an export means inserting it into its
|
|
131
|
+
alphabetical slot, not appending to the end. This holds for every barrel:
|
|
132
|
+
segment barrels (`src/shared/ui/<layer>/index.ts`), `hooks/` barrels, and
|
|
133
|
+
component `index.ts` files alike.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
export * from './alert'
|
|
137
|
+
export * from './avatar'
|
|
138
|
+
export * from './badge'
|
|
139
|
+
export * from './button'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## @mediacubeco libraries
|
|
143
|
+
|
|
144
|
+
The `@mediacubeco/*` packages are the project's utility belt. **Everything that
|
|
145
|
+
exists there must be imported from there — duplicating or re-implementing it is
|
|
146
|
+
a defect.** The packages evolve between versions, so never rely on a memorized
|
|
147
|
+
list; inspect the installed version:
|
|
148
|
+
|
|
149
|
+
- Discover the export surface via the DTS indexes in `node_modules`:
|
|
150
|
+
`@mediacubeco/base` (`dist/index.d.ts`, subpaths `/helpers`, `/services`,
|
|
151
|
+
`/types`, `/utils`) and `@mediacubeco/base-react-native` (subpaths
|
|
152
|
+
`/helpers`, `/hooks`, `/utils`). Grep `from '@mediacubeco/` across `src/`
|
|
153
|
+
to see the patterns in active use.
|
|
154
|
+
- Always import via subpath, never from the package root:
|
|
155
|
+
`@mediacubeco/base/utils` ✅, `@mediacubeco/base` ❌.
|
|
156
|
+
- **Before hand-rolling any utility hook or helper — layout measurement,
|
|
157
|
+
keyboard, app-state, debounce, guards, and the like — inspect the library
|
|
158
|
+
first and use what it already provides; re-implementing what exists there is a
|
|
159
|
+
defect.** Read the installed exports and their return shapes (per the DTS
|
|
160
|
+
inspection above) instead of assuming an API — the package evolves, so no
|
|
161
|
+
specific export is guaranteed to exist: discover it, don't memorize it.
|
|
162
|
+
- Optional callbacks are invoked via the package's callback utility, never
|
|
163
|
+
`onPress?.(event)`.
|
|
164
|
+
- Never copy source from a `@mediacubeco/*` package into the app. If something
|
|
165
|
+
is missing, add it to the package first, then import it.
|
|
166
|
+
- ESLint, Prettier, and TypeScript configs extend the corresponding
|
|
167
|
+
`@mediacubeco/*` config package — never inline equivalent rules.
|
|
168
|
+
|
|
169
|
+
## Components
|
|
170
|
+
|
|
171
|
+
- Arrow-function components: `const Name = (props: NameProps) => { ... }`,
|
|
172
|
+
default-exported at the bottom.
|
|
173
|
+
- **All visual logic lives in `useXxxStyles()`** — not just `StyleSheet.create`,
|
|
174
|
+
but also layout measurement, shared values, `useAnimatedStyle`, derived design
|
|
175
|
+
constants, and layout/scroll handlers. The component file keeps only behavior
|
|
176
|
+
(gestures, callbacks) and markup. This keeps `.tsx` declarative and lets the
|
|
177
|
+
styles hook be read as the single source of visual truth.
|
|
178
|
+
- All visual values come from theme tokens via `useThemeMode()`. Hardcoding
|
|
179
|
+
any visual value is forbidden — if it affects how something looks, it comes
|
|
180
|
+
from a token. Never inline styles in JSX.
|
|
181
|
+
- **Theme and measurement hooks never appear in the component file** —
|
|
182
|
+
`useTheme`, `useThemeMode`, `useSafeArea`, `useBreakpoints`, `useMixins` and
|
|
183
|
+
the like are called only inside `useXxxStyles()`. If the component needs a
|
|
184
|
+
dynamic color or size (e.g. `placeholderTextColor`), the styles hook returns
|
|
185
|
+
it.
|
|
186
|
+
- The styles hook returns **only what the component actually uses** —
|
|
187
|
+
`{ styles }` plus animated styles, design constants, handlers, colors as
|
|
188
|
+
needed. Nothing speculative.
|
|
189
|
+
- **Prefer a parameterless styles hook — `useXxxStyles()` with no arguments.**
|
|
190
|
+
When a visual variation is a fixed, finite set (sizes, states, variants),
|
|
191
|
+
build **every** variant inside the hook (e.g. a `stylesBySize` map keyed by
|
|
192
|
+
the variant) and let the component pick the relevant one by key — don't
|
|
193
|
+
thread the current variant into the hook as a context. A hook argument
|
|
194
|
+
(a `<Name>StylesContext`) is justified only when a style depends on a runtime
|
|
195
|
+
value that _cannot_ be enumerated ahead of time (a measured dimension, a
|
|
196
|
+
free-form color). If you can pass nothing, pass nothing.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
// hook builds all variants, takes no arguments
|
|
200
|
+
const useCardStyles = () => {
|
|
201
|
+
const { SIZES } = useThemeMode()
|
|
202
|
+
|
|
203
|
+
const stylesBySize = StyleSheet.create({
|
|
204
|
+
small: { width: SIZES['6XL'], borderRadius: SIZES['6XL'] },
|
|
205
|
+
large: { width: SIZES['14XL'], borderRadius: SIZES['14XL'] },
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
return { stylesBySize }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// component selects the variant by key
|
|
212
|
+
const Card = ({ size = 'small' }: CardProps) => {
|
|
213
|
+
const { stylesBySize } = useCardStyles()
|
|
214
|
+
|
|
215
|
+
return <View style={stylesBySize[size]} />
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Shared UI components (`src/shared/ui`)
|
|
220
|
+
|
|
221
|
+
Canonical scaffolds live in `references/components/` — read them before
|
|
222
|
+
creating a component:
|
|
223
|
+
|
|
224
|
+
- `references/components/post/` — simple custom component: own props, `Text`
|
|
225
|
+
atom with variants instead of raw RN `Text`, styles only for layout.
|
|
226
|
+
- `references/components/wrap/` — **native wrapper pattern**: extends the
|
|
227
|
+
native props with `Omit<NativeProps, 'style'>`, swallows `style` in
|
|
228
|
+
destructuring, exposes the `outerStyle` / `innerStyle` pair (inner `View`
|
|
229
|
+
wraps children), forwards `ref` to the native element, defaults set in
|
|
230
|
+
destructuring (`decelerationRate = 'fast'`).
|
|
231
|
+
|
|
232
|
+
Rules:
|
|
233
|
+
|
|
234
|
+
- Place at the lowest atomic layer that fits: `atoms/` (single-purpose
|
|
235
|
+
primitive) → `molecules/` (combines atoms) → `organisms/` (self-contained
|
|
236
|
+
block) → `templates/` (full-screen scaffold).
|
|
237
|
+
- Four files per component: `<name>.tsx`, `<name>.types.ts`, `<name>.styles.ts`,
|
|
238
|
+
`index.ts`. The `index.ts` re-exports explicitly:
|
|
239
|
+
`export { default as <Name> } from './<name>'` and
|
|
240
|
+
`export type { <Name>Props } from './<name>.types'` — named types only,
|
|
241
|
+
`*StylesContext` stays internal.
|
|
242
|
+
- Register the component in the parent layer barrel
|
|
243
|
+
(`src/shared/ui/<layer>/index.ts`), alphabetically.
|
|
244
|
+
- `<name>.types.ts` exports only types. Props type even when empty
|
|
245
|
+
(`type KeyboardAreaProps = {}`). When the styles hook does take a context
|
|
246
|
+
(only for non-enumerable runtime values — see "Prefer a parameterless styles
|
|
247
|
+
hook" above), it is derived from props:
|
|
248
|
+
`type <Name>StylesContext = Required<Pick<<Name>Props, '…'>> & {}`.
|
|
249
|
+
- **One file, one component — every subcomponent lives in its own file.** A
|
|
250
|
+
component declared inside another component's file is a defect: extract it.
|
|
251
|
+
Any subcomponent internal to a component — of **any** layer (atom, molecule,
|
|
252
|
+
organism, template), not just organisms — lives in a nested `components/`
|
|
253
|
+
folder (`<name>/components/<subcomponent>/…`), each subcomponent as its own
|
|
254
|
+
4-file folder with its own `index.ts`; a `components/index.ts` barrel
|
|
255
|
+
re-exports them alphabetically. Never keep several components in one file.
|
|
256
|
+
- Multi-variant components keep one folder with a file per variant plus shared
|
|
257
|
+
`<name>.types.ts(x)`, `<name>.styles.ts`, and `<name>.hooks.tsx` for shared
|
|
258
|
+
prop-building logic.
|
|
259
|
+
- Compose from existing shared atoms (`Text` with a `variant` instead of raw
|
|
260
|
+
RN `Text`); typography lives in the `Text` atom, not in component styles.
|
|
261
|
+
|
|
262
|
+
### Memoization
|
|
263
|
+
|
|
264
|
+
- `useCallback` — **always**, for every handler.
|
|
265
|
+
- `useMemo` — only for genuinely expensive computations (building arrays,
|
|
266
|
+
gesture configs). Trivial derivations stay plain.
|
|
267
|
+
- `memo()` on components — only for heavy components; not by default.
|
|
268
|
+
|
|
269
|
+
### JSX
|
|
270
|
+
|
|
271
|
+
- Prop values are always explicit and braced: `horizontal={true}`,
|
|
272
|
+
`decelerationRate={'fast'}`. Never bare booleans or quoted strings.
|
|
273
|
+
- **Conditional rendering of a single branch uses `&&`** — a ternary with
|
|
274
|
+
`: null` is noise. The ternary is reserved for a genuine two-way choice
|
|
275
|
+
(two real render branches or two values).
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
// ✅ one branch — &&
|
|
279
|
+
{
|
|
280
|
+
value && (
|
|
281
|
+
<Wrap>
|
|
282
|
+
<Component />
|
|
283
|
+
</Wrap>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ❌ one branch — ternary with null
|
|
288
|
+
{
|
|
289
|
+
value ? (
|
|
290
|
+
<Wrap>
|
|
291
|
+
<Component />
|
|
292
|
+
</Wrap>
|
|
293
|
+
) : null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ✅ two branches — ternary
|
|
297
|
+
{
|
|
298
|
+
isLoading ? <Spinner /> : <Content />
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Guard against accidental `0`/`''` renders by making the condition boolean
|
|
303
|
+
when the value is a number or string count: `{itemCount > 0 && <Badge />}`.
|
|
304
|
+
|
|
305
|
+
- Prop order, both in destructuring and in JSX attributes:
|
|
306
|
+
1. `ref`
|
|
307
|
+
2. data — within the group ordered by type: strings → numbers → booleans →
|
|
308
|
+
union values → arrays (`*List`) → objects
|
|
309
|
+
3. styles (`style`, `outerStyle`, `innerStyle`, `contentContainerStyle`)
|
|
310
|
+
4. configuration flags
|
|
311
|
+
5. handlers (`onX`) and render props (`renderItem`) — always last
|
|
312
|
+
|
|
313
|
+
## Visual tokens
|
|
314
|
+
|
|
315
|
+
How the design decisions from the `design-style` skill are written down in
|
|
316
|
+
code. Every visual value comes from `useThemeMode()` tokens (read only inside
|
|
317
|
+
`useXxxStyles`); a raw hex/rgba, number, duration, or font in a page, widget,
|
|
318
|
+
or feature is a defect — it silently breaks the second theme and the accent
|
|
319
|
+
system.
|
|
320
|
+
|
|
321
|
+
### Colors (`COLORS`)
|
|
322
|
+
|
|
323
|
+
- Roles, not palettes: `BACKGROUND` (screen planes), `FILL` (surfaces),
|
|
324
|
+
`STROKE` (borders/dividers), `TYPOGRAPHY` (text/icons), plus `BAR` and
|
|
325
|
+
`INPUT` for their namesake components. Pick the token by what the element
|
|
326
|
+
_is_.
|
|
327
|
+
- Intensity steps: base → `_WEAK` (80%) → `_HALF` (40%) → `_STRONG` (20%) →
|
|
328
|
+
`_LIGHT` (10%). A lighter shade of a semantic color is its alpha step,
|
|
329
|
+
never a new color.
|
|
330
|
+
- `PRIMARY*` is derived from the user-chosen accent through the theme
|
|
331
|
+
factories — anything accent-colored uses `PRIMARY` tokens.
|
|
332
|
+
- Paired content colors: content on accent uses `PRIMARY_CONTRAST`, on dark
|
|
333
|
+
surfaces `CONTRAST`, on light surfaces `MATTER` or `TYPOGRAPHY.BASE`.
|
|
334
|
+
- A new color concept is added to the palette and threaded through the
|
|
335
|
+
light/dark factory functions — never placed beside the system as a local
|
|
336
|
+
constant. The test: switching theme mode and accent must recolor it with
|
|
337
|
+
zero extra work at call sites. The single exception — artistic hero-scene
|
|
338
|
+
colors — are named `UPPER_SNAKE_CASE` constants at the top of the owning
|
|
339
|
+
styles file with a JSDoc explaining they are deliberate art, defined once
|
|
340
|
+
(shared scenes share constants).
|
|
341
|
+
- Previews of another theme/accent resolve the actual theme object for that
|
|
342
|
+
mode/accent and read its `COLORS` — never hand-picked approximations.
|
|
343
|
+
|
|
344
|
+
### Sizes, radii, and spacing (`SIZES`)
|
|
345
|
+
|
|
346
|
+
All radii and spacing come from `SIZES` or are computed from `SIZES`-based
|
|
347
|
+
dimensions, as `UPPER_SNAKE_CASE` constants in the styles hook with a JSDoc
|
|
348
|
+
deriving them (see the radii in
|
|
349
|
+
`references/components/payout-speed-option/payout-speed-option.styles.ts`).
|
|
350
|
+
|
|
351
|
+
- Capsule radius = element height / 2, computed from the same sizes that
|
|
352
|
+
build the element (icon size + paddings).
|
|
353
|
+
- Concentric nesting: `radius = container radius − gap to the container
|
|
354
|
+
edge`; a frame around rounded content: `content radius + frame padding`.
|
|
355
|
+
- Card radii land at `SIZES['5XL']` and above; `SIZES['3XL']` and smaller
|
|
356
|
+
belong to _nested_ tiles inside a card, never to the card itself.
|
|
357
|
+
- The spacing rhythm, outermost to innermost:
|
|
358
|
+
|
|
359
|
+
| Level | Token | Use |
|
|
360
|
+
| --------------- | ----- | -------------------------------------------------------------------------------- |
|
|
361
|
+
| Screen sections | `4XL` | gap between independent blocks on a screen |
|
|
362
|
+
| Inside a card | `2XL` | card padding |
|
|
363
|
+
| Row content | `XL` | gap between icon / text / trailing control in a row; horizontal insets of labels |
|
|
364
|
+
| Small stacks | `L` | gap between sibling tiles, label-to-card margin |
|
|
365
|
+
| Text pairs | `3XS` | value + caption; line height already provides the air |
|
|
366
|
+
|
|
367
|
+
### Typography (`Text`)
|
|
368
|
+
|
|
369
|
+
All text goes through the `Text` atom — its `type` prop is the entire scale
|
|
370
|
+
(`bigTitle`, `h1`–`h3`, `subtitle`, `body`/`bodySB`,
|
|
371
|
+
`description`/`descriptionSB`, `small`/`smallSB`, `button`), and it already
|
|
372
|
+
carries the tight negative letter-spacing. A raw `fontSize`/`fontFamily` in
|
|
373
|
+
feature styles is a defect. Captions use `variant={'secondary'}`; long
|
|
374
|
+
content uses `lineCount` (cap lines) and `shouldFit` (shrink one-liners).
|
|
375
|
+
|
|
376
|
+
**A text placeholder is sized from the type it replaces.** A skeleton plate
|
|
377
|
+
standing in for a line of `Text` gets the **exact height of that type's line
|
|
378
|
+
box**: the same `SIZES` token as the type's `fontSize`, times the scale's
|
|
379
|
+
line-height ratio (`* 1.2`) — the identical expression the `Text` scale uses to
|
|
380
|
+
compute its own `lineHeight`. So a `bodySB` line → `SIZES['2XL'] * 1.2`, an
|
|
381
|
+
`outlineSB` line → `SIZES['XL-2XL'] * 1.2`, a `small` line →
|
|
382
|
+
`SIZES['L-XL'] * 1.2`. Width may be approximate (a fraction that reads as a line
|
|
383
|
+
of text); height must equal the line box exactly, so the real text drops in with
|
|
384
|
+
zero vertical reflow (design skill, "Loading" — placeholders match final size).
|
|
385
|
+
Never hardcode a skeleton height in raw pixels: mirror the token and the `* 1.2`
|
|
386
|
+
ratio of the covered type, and the placeholder tracks the scale if that type
|
|
387
|
+
ever changes.
|
|
388
|
+
|
|
389
|
+
### Materials (`Glass`, `Blur`, `SHADOWS`)
|
|
390
|
+
|
|
391
|
+
- The `Glass` atom is used only for the floating navigation layer (screen
|
|
392
|
+
header, bottom bar); the `Blur` atom for scrims and overlays. Everything
|
|
393
|
+
else is opaque on `FILL`/`BACKGROUND` tokens.
|
|
394
|
+
- Shadows come from `SHADOWS`: `THIN` for barely-lifted elements,
|
|
395
|
+
`BASE`/`FAT` only for genuinely floating surfaces.
|
|
396
|
+
|
|
397
|
+
### Motion (`ANIMATIONS`) and press feedback (`Touch`)
|
|
398
|
+
|
|
399
|
+
- State changes animate over `ANIMATIONS.TIMING.BASE`, micro-feedback uses
|
|
400
|
+
`SHORT`, `LONG` is reserved for screen-scale transitions; springs use
|
|
401
|
+
`ANIMATIONS.SPRING.BASE`. `withTiming()`/`withSpring()` may be called
|
|
402
|
+
without a config (library defaults); when a config **is** passed, its
|
|
403
|
+
values come from `ANIMATIONS` tokens — a raw duration is a defect.
|
|
404
|
+
- The `Touch` atom provides press feedback; pick the prop by role:
|
|
405
|
+
`withRipple` for rows inside containers and small utility targets,
|
|
406
|
+
`withSpring` (scale ~0.95) for self-contained cards and standalone
|
|
407
|
+
elements, `withHaptics` only for significant actions. `Button` animates
|
|
408
|
+
itself — never wrap it in extra feedback.
|
|
409
|
+
- Disabled state goes through `DISABLED` tokens and the component's
|
|
410
|
+
`isDisabled` prop, never a hand-tuned opacity; a disabled element gets no
|
|
411
|
+
feedback props at all.
|
|
412
|
+
|
|
413
|
+
### The selection scaffold
|
|
414
|
+
|
|
415
|
+
Canonical example: `references/components/payout-speed-option/` — the
|
|
416
|
+
project's selection language in code. Every "pick one of N" component follows
|
|
417
|
+
it:
|
|
418
|
+
|
|
419
|
+
- an animated frame:
|
|
420
|
+
`interpolateColor(STROKE.TRANSPARENT → STROKE.PRIMARY)` on a shared value
|
|
421
|
+
driven by `handleActive`/`handleInactive` over `TIMING.BASE`, with a
|
|
422
|
+
constant `SIZES['S']` border width (color changes, layout never jumps);
|
|
423
|
+
- a check indicator with paired `scale` 0→1 + `opacity` 0→1 on the same
|
|
424
|
+
shared value;
|
|
425
|
+
- `withHaptics` on the commit.
|
|
426
|
+
|
|
427
|
+
## Hooks
|
|
428
|
+
|
|
429
|
+
Canonical example: `references/hooks/use-posts.ts` — a custom domain hook
|
|
430
|
+
showing the full pattern: a private unexported helper hook, a derived
|
|
431
|
+
`postList` in `useMemo`, a `handleX` callback in `useCallback`, and
|
|
432
|
+
composition of an API hook. Read it before writing any custom hook.
|
|
433
|
+
|
|
434
|
+
- One public hook per file, file named `use-<name>.ts` — the default. Private
|
|
435
|
+
helper hooks may live unexported in the same file (see the unexported
|
|
436
|
+
`useSearch` in `references/hooks/use-posts.ts`). Justified exceptions exist —
|
|
437
|
+
e.g. the API layer groups all hooks of one resource in a single
|
|
438
|
+
`use-api-<resource>.ts` — but don't invent new ones without a reason.
|
|
439
|
+
- Placement: app-wide reusable hooks → `src/shared/hooks/`; hooks of a shared
|
|
440
|
+
lib domain → `src/shared/lib/<domain>/hooks/`; domain-specific hooks → the
|
|
441
|
+
FSD slice that owns the domain. Every `hooks/` folder has an alphabetical
|
|
442
|
+
barrel `index.ts` of `export * from './use-…'` lines.
|
|
443
|
+
- Same export policy as everywhere: `const useX = () => { … }`, then
|
|
444
|
+
`export { useX }` at the bottom — hooks are never default-exported.
|
|
445
|
+
- A hook that exposes several things returns a single object, fields ordered
|
|
446
|
+
like JSX data props (strings → numbers → booleans → arrays → objects,
|
|
447
|
+
handlers last). A hook whose natural API is one thing returns that thing
|
|
448
|
+
directly — `useLocalizationText` returns the `t` function,
|
|
449
|
+
`useNavigationRoute` returns the router facade. Don't force a wrapper
|
|
450
|
+
object around a single value, and don't hand-write (or `ReturnType`-export)
|
|
451
|
+
return types TypeScript already derives.
|
|
452
|
+
- Every function returned from a hook is wrapped in `useCallback`; derived
|
|
453
|
+
arrays/objects in `useMemo` — the same memoization policy as components.
|
|
454
|
+
- Context hooks fail fast outside their provider via `ErrorHandler.throw`
|
|
455
|
+
from `@mediacubeco/base/helpers`. A context designed with a meaningful
|
|
456
|
+
default value (e.g. an availability flag defaulting to
|
|
457
|
+
`{ isAvailable: false }`) may return that default instead of throwing —
|
|
458
|
+
but that must be a deliberate design, not a forgotten check.
|
|
459
|
+
- Third-party hooks are wrapped in thin project facades instead of being
|
|
460
|
+
called at use sites: `useNavigationRoute` wraps `useRouter`,
|
|
461
|
+
`useLocalizationText` wraps `useLocalization`. Call sites depend on project
|
|
462
|
+
vocabulary, and the dependency can be swapped in one file.
|
|
463
|
+
- Component-scoped hooks stay next to their component — `useXxxStyles` in
|
|
464
|
+
`<name>.styles.ts`, shared prop-building `useXxxProps` in
|
|
465
|
+
`<name>.hooks.tsx` — never in a `hooks/` folder.
|
|
466
|
+
- Any work in the API layer (`src/shared/lib/api` — resources, entities,
|
|
467
|
+
repositories, queries, hooks, mocks) goes through the **api** skill, which owns
|
|
468
|
+
the data-layer architecture (plain resources in the repository, entities in
|
|
469
|
+
`select`, `queryOptions` factories, transport options). Don't hand-roll fetch
|
|
470
|
+
calls, query keys, or TanStack Query wiring.
|
|
471
|
+
|
|
472
|
+
## State
|
|
473
|
+
|
|
474
|
+
Client state uses **Zustand** — a small store per concern. There is no central
|
|
475
|
+
store; state is owned by the slice it belongs to. Canonical example:
|
|
476
|
+
`references/store/use-post-draft-store.ts` — read it before adding or touching
|
|
477
|
+
any store.
|
|
478
|
+
|
|
479
|
+
Pick the smallest tier that fits. Reaching for a store when local state or the
|
|
480
|
+
query cache would do is the most common mistake here:
|
|
481
|
+
|
|
482
|
+
1. **Server state → TanStack Query.** Anything from the API — lists, entities,
|
|
483
|
+
mutations, their loading/error — lives in the query cache and goes through
|
|
484
|
+
the **api** skill. Never copy server data into a store; a store that
|
|
485
|
+
mirrors the server is a defect.
|
|
486
|
+
2. **Client state that outlives one component _or_ is read/written outside
|
|
487
|
+
React → a Zustand store** (session tokens, a multi-step form draft,
|
|
488
|
+
cross-screen UI state).
|
|
489
|
+
3. **Everything else → local `useState` / `useReducer`, or React Context** for
|
|
490
|
+
DI-style values. Theme, navigation, and websocket already use Context —
|
|
491
|
+
that is correct, do not migrate them to a store.
|
|
492
|
+
|
|
493
|
+
### Where a store lives
|
|
494
|
+
|
|
495
|
+
- Domain state → the **`model/` segment of the owning slice**
|
|
496
|
+
(`features/<name>/model/`, `entities/<name>/model/`).
|
|
497
|
+
- Cross-cutting / app-global state (e.g. the auth session) → the dedicated
|
|
498
|
+
**`src/shared/lib/store/`** lib.
|
|
499
|
+
- File is kebab-case `use-<domain>-store.ts`; the hook is `use<Domain>Store`;
|
|
500
|
+
re-export it from the segment barrel. One store = one concern — small,
|
|
501
|
+
independent stores rather than one big object.
|
|
502
|
+
|
|
503
|
+
### Writing a store
|
|
504
|
+
|
|
505
|
+
- `create<State>()(…)` with a typed state: boolean prefixes and `List`
|
|
506
|
+
suffixes per the naming rules, actions as methods on the state object,
|
|
507
|
+
Russian JSDoc on the state type, every field, and every action.
|
|
508
|
+
- Persist **only** state that must survive a restart: `persist` middleware +
|
|
509
|
+
`createJSONStorage` pointed at the shared adapters from
|
|
510
|
+
`~shared/lib/storage` — never instantiate MMKV, SecureStore, or AsyncStorage
|
|
511
|
+
inside a slice. Sensitive data (tokens) → `storageSecureAsync`;
|
|
512
|
+
non-sensitive → `storageAsync`. Use `partialize` to keep transient fields
|
|
513
|
+
out of storage (see the reference).
|
|
514
|
+
|
|
515
|
+
### Consuming a store
|
|
516
|
+
|
|
517
|
+
- In components — **always through a selector**:
|
|
518
|
+
`useSessionStore((state) => state.accessToken)`. Never grab the whole store
|
|
519
|
+
(`const state = useSessionStore()`) — that re-renders on every field change.
|
|
520
|
+
- Outside React — the vanilla store API: `useSessionStore.getState()`,
|
|
521
|
+
`.setState()`, `.subscribe()`. This is the sanctioned way to reach client
|
|
522
|
+
state from non-React code (e.g. the API transport attaching the
|
|
523
|
+
`Authorization` header).
|
|
524
|
+
|
|
525
|
+
### Boundaries
|
|
526
|
+
|
|
527
|
+
- **Stores never import each other.** A cross-slice need is composed in a
|
|
528
|
+
consuming hook that reads both stores; the FSD import direction
|
|
529
|
+
(`features → entities → shared`) still holds.
|
|
530
|
+
- A store holds **client** state only. If you are tempted to cache a request
|
|
531
|
+
result in it, that data belongs in TanStack Query.
|
|
532
|
+
|
|
533
|
+
## Routes (`src/app`, Expo Router)
|
|
534
|
+
|
|
535
|
+
**`src/app` stays maximally empty.** Files under it only wire routes to
|
|
536
|
+
implementation — screen UI, business logic, state, and helpers live in
|
|
537
|
+
`~pages/*` and lower layers. No extra logic, no extra dependencies, no
|
|
538
|
+
components defined in place: anything beyond a thin wrapper or a navigator
|
|
539
|
+
declaration is a defect. When you notice logic accumulating in `src/app` (or
|
|
540
|
+
are asked to add some), flag it and propose where it belongs instead.
|
|
541
|
+
|
|
542
|
+
Canonical examples: `references/routes/` (read them before creating or editing
|
|
543
|
+
any `src/app` file):
|
|
544
|
+
|
|
545
|
+
- `references/routes/_app.tsx` + `share.tsx`, `guide.tsx` — the root stack:
|
|
546
|
+
`_app.tsx` holds the root Stack with `Stack.Protected` and the root
|
|
547
|
+
registrations of globally reachable routes; `share.tsx` / `guide.tsx` are
|
|
548
|
+
the same thin wrappers as the domain ones.
|
|
549
|
+
- `references/routes/(stack)/` — Stack navigator: `_layout.tsx` (component
|
|
550
|
+
named `Layout`, options via `useNavigationStackOptions()`) plus its screens:
|
|
551
|
+
`index.tsx` (post list), `details.tsx` (post details), a sheet modal
|
|
552
|
+
`share.tsx` registered with `sheetOptions`, and a WebView screen `guide.tsx`
|
|
553
|
+
registered with `webOptions`.
|
|
554
|
+
- `references/routes/(tabs)/` — NativeTabs navigator: `_layout.tsx` with
|
|
555
|
+
`useNavigationTabBarNativeOptions()` / `useNavigationTabBarOptions()` and
|
|
556
|
+
the `NavigationTabsNativeProvider` / `NavigationTabsProvider` pair; each tab
|
|
557
|
+
points to a group folder (`(post-list)/`, `(post-details)/`) holding that
|
|
558
|
+
tab's own screens.
|
|
559
|
+
- Route screen files (`index.tsx`, `details.tsx`, `[param].tsx`, …):
|
|
560
|
+
component named `Page`, renders a single root component imported from
|
|
561
|
+
`~pages/...`, nothing else.
|
|
562
|
+
|
|
563
|
+
### Screen options
|
|
564
|
+
|
|
565
|
+
`useNavigationStackOptions()` returns four ready option objects and a
|
|
566
|
+
`createXxxOptions(overrides)` factory for each:
|
|
567
|
+
|
|
568
|
+
| Options | Factory | Use |
|
|
569
|
+
| --------------- | ----------------------- | ----------------------------------------------------- |
|
|
570
|
+
| `stackOptions` | `createStackOptions()` | the `screenOptions` of a `Stack` navigator |
|
|
571
|
+
| `screenOptions` | `createScreenOptions()` | a regular push screen |
|
|
572
|
+
| `sheetOptions` | `createSheetOptions()` | a sheet modal (`formSheet`, fit-to-contents, grabber) |
|
|
573
|
+
| `webOptions` | `createWebOptions()` | a WebView screen (full-height `formSheet`) |
|
|
574
|
+
|
|
575
|
+
Pass the ready object when no customization is needed
|
|
576
|
+
(`options={sheetOptions}`); reach for the factory only to override specific
|
|
577
|
+
options in place (`options={createStackOptions({ animation: 'none' })}`).
|
|
578
|
+
|
|
579
|
+
### Rules
|
|
580
|
+
|
|
581
|
+
- `_layout.tsx` holds navigation and options only — never content markup.
|
|
582
|
+
- **Every screen is registered explicitly with its own `<Stack.Screen>` in its
|
|
583
|
+
stack's `_layout.tsx`** — with the options object its role calls for
|
|
584
|
+
(`screenOptions` for a push screen, `sheetOptions` for a modal, `webOptions`
|
|
585
|
+
for a web view). Expo Router auto-discovers a route file even without a
|
|
586
|
+
`<Stack.Screen>` line, so a missing registration does not break navigation —
|
|
587
|
+
it silently falls back to the navigator's default `stackOptions`, and the
|
|
588
|
+
screen quietly diverges from its siblings (wrong content background, no
|
|
589
|
+
animation/gesture config). That silent drift is the defect: a route file with
|
|
590
|
+
no matching `<Stack.Screen>` in its `_layout.tsx` is incomplete. The layout is
|
|
591
|
+
the full, readable census of the stack — every route file appears there once,
|
|
592
|
+
and every registration points at a real route file.
|
|
593
|
+
- **Registrations are sorted alphabetically by `name`**, in one flat list — the
|
|
594
|
+
same ordering as the route files on disk and the page barrel (no grouping by
|
|
595
|
+
sub-section, no blank-line blocks). The `_layout` then reads as a mirror of the
|
|
596
|
+
folder, so a screen is found the same way in both. The only splits allowed are
|
|
597
|
+
structural navigator branches (`Stack.Protected` guards in the root stack, the
|
|
598
|
+
`if`/`else` native-vs-JS tab bar) — sort alphabetically **within** each branch,
|
|
599
|
+
never regroup across one.
|
|
600
|
+
- Routes are named after the business domain: group folders `(post-list)`,
|
|
601
|
+
titles from the domain's translation namespace (`t('post.list.title')`,
|
|
602
|
+
`t('post.details.title')`).
|
|
603
|
+
- **A route segment is the screen's own name, not its sub-section.** The route
|
|
604
|
+
file (`tags.tsx`) and its `Stack.Screen name` (`'tags'`) carry only the
|
|
605
|
+
screen's own identity — the domain stack's `_layout.tsx` already expresses the
|
|
606
|
+
section, so `settings-tags.tsx` / `name={'settings-tags'}` is a defect. This
|
|
607
|
+
mirrors the page-naming rule under "Pages": the same screen is `post-tags/` in
|
|
608
|
+
`~pages/` and `tags` as a route. Links target that flat segment
|
|
609
|
+
(`/(post)/tags`), which is what keeps a screen movable between sub-menus
|
|
610
|
+
without touching its name.
|
|
611
|
+
- Route screen files never duplicate screen markup — only the thin wrapper.
|
|
612
|
+
- **Modals (sheets) and web screens are routes like any other.** There is no
|
|
613
|
+
global modal registry: the route is registered in the `_layout.tsx` of the
|
|
614
|
+
domain stack that owns its business logic, next to that domain's screens,
|
|
615
|
+
with `sheetOptions` (or `webOptions` for WebView screens). This keeps the
|
|
616
|
+
whole domain — screens, its modals, its web views — discoverable in one
|
|
617
|
+
layout. The sheet hides the native header, so no `title` is passed.
|
|
618
|
+
- In `~pages/` a modal or web screen is **not** marked in any way — no folder,
|
|
619
|
+
no name suffix; it is a regular page component of its domain. The only
|
|
620
|
+
difference is the root element: a `ModalContainer*` template
|
|
621
|
+
(`ModalContainerScroll` / `ModalContainerList` /
|
|
622
|
+
`ModalContainerSectionList`) for modals, `WebContainer` for web screens,
|
|
623
|
+
instead of `ScreenContainer*`.
|
|
624
|
+
- **A route that must open over any context** (push notification, deep link,
|
|
625
|
+
cross-domain call) — a modal, a web screen, or a regular screen — is
|
|
626
|
+
_additionally_ registered in the root stack in `_app.tsx`, inside
|
|
627
|
+
`Stack.Protected`, with the same thin route file and the same options object
|
|
628
|
+
— no local option overrides on either copy. The two routes are two
|
|
629
|
+
behaviors, not duplication: the domain route opens in its domain context,
|
|
630
|
+
the root route opens over whatever screen is current. The root URL is the
|
|
631
|
+
canonical entry for everything external (pushes, links, analytics);
|
|
632
|
+
in-domain flows navigate to the domain route.
|
|
633
|
+
- Both `Layout` and `Page` are default-exported at the end of the file
|
|
634
|
+
(Expo Router requirement, same export policy as everywhere).
|
|
635
|
+
- Special Expo Router files (`+not-found.tsx`, `+html.tsx`, …) follow their own
|
|
636
|
+
contract from the Expo docs.
|
|
637
|
+
|
|
638
|
+
### Multi-screen flows: back vs close
|
|
639
|
+
|
|
640
|
+
A **flow** is a sequence of pushed screens that together complete one task
|
|
641
|
+
(enter phone → confirm code → success). Its screens split into two roles, and
|
|
642
|
+
the header reflects the role:
|
|
643
|
+
|
|
644
|
+
- **Every step except the last** carries a **back** control — an
|
|
645
|
+
`ScreenHeaderIconBack` in the header's **`left`** slot. It steps one screen
|
|
646
|
+
back (`navigation.back()`), letting the user revise the previous step.
|
|
647
|
+
- **The last screen** (the success/result step) carries a **close** control
|
|
648
|
+
instead — an `ScreenHeaderIconClose` in the header's **`right`** slot, and
|
|
649
|
+
**no** back on the left. From the end of a flow, stepping back into the
|
|
650
|
+
just-completed steps is wrong; the screen is terminal. Close dismisses the
|
|
651
|
+
whole flow at once, back to **the screen the flow started from** (its first
|
|
652
|
+
screen), via `navigation.dismissTo(firstScreenHref)`.
|
|
653
|
+
|
|
654
|
+
```tsx
|
|
655
|
+
// last screen of a flow — close (right), no back (left)
|
|
656
|
+
<ScreenContainerScroll
|
|
657
|
+
screenHeader={<ScreenHeader right={<ScreenHeaderIconClose to={'/(profile)/security'} />} />}
|
|
658
|
+
>
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
`ScreenHeaderIconClose` takes the flow's first-screen `to` href and collapses
|
|
662
|
+
the stack to it (without a `to` it exits to the stack root); pass the same href
|
|
663
|
+
the flow was launched from. When closing must also do flow cleanup (resetting a
|
|
664
|
+
wizard store), keep that in the screen's own handler rather than the shared
|
|
665
|
+
icon. Back on the left, close on the right — never both, never a back on a
|
|
666
|
+
terminal screen.
|
|
667
|
+
|
|
668
|
+
## Pages (`src/pages`)
|
|
669
|
+
|
|
670
|
+
Canonical scaffold: `references/pages/post/` — a domain folder with four pages:
|
|
671
|
+
`post-list/` (with a styles hook), `post-details/` (without one), `post-share/`
|
|
672
|
+
(a modal page on `ModalContainerScroll`), and `post-guide/` (a web page on
|
|
673
|
+
`WebContainer`). Read it before creating or restructuring any page.
|
|
674
|
+
|
|
675
|
+
- **Every page is a folder, never a flat file.** A page lives at
|
|
676
|
+
`src/pages/<domain>/<domain>-<name>/` (kebab-case, prefixed with its domain:
|
|
677
|
+
`post-list`, not `list`). Markup, styles, and future page-local files all
|
|
678
|
+
live inside that folder — a bare `<domain>-<name>.tsx` next to the domain
|
|
679
|
+
barrel is a defect.
|
|
680
|
+
- **Name a screen from its domain and itself — never from the sub-section it
|
|
681
|
+
currently sits in.** The `<name>` part is the screen's own identity, and the
|
|
682
|
+
prefix is the top-level domain, not the intermediate menu/group the screen
|
|
683
|
+
happens to live under today. Screens migrate between sub-sections, and a name
|
|
684
|
+
that bakes in the grouping goes stale the moment they do. A screen that edits
|
|
685
|
+
a post's tags is `post-tags/` (component `PostTags`, route `tags`) whether it
|
|
686
|
+
is reached from a "settings" sub-menu or a "security" sub-menu — never
|
|
687
|
+
`post-settings-tags` / `PostSettingsTags`. The sub-section is expressed by
|
|
688
|
+
**where** the route is registered and **how** it is reached, not by the name.
|
|
689
|
+
When two screens would collide under this rule (e.g. a screen and its
|
|
690
|
+
confirmation modal both reducing to `post-delete`), disambiguate by their
|
|
691
|
+
distinct role — `post-delete` for the screen, `post-delete-confirm` for the
|
|
692
|
+
modal — never by re-introducing the sub-section prefix.
|
|
693
|
+
- Files inside the page folder:
|
|
694
|
+
- `<domain>-<name>.tsx` — the page component, named `<Domain><Name>`
|
|
695
|
+
(`PostList`), default-exported at the bottom;
|
|
696
|
+
- `index.ts` — explicit re-export:
|
|
697
|
+
`export { default as PostList } from './post-list'`;
|
|
698
|
+
- `<domain>-<name>.styles.ts` — appears together with the first piece of
|
|
699
|
+
visual logic, holding the `useXxxStyles` hook (same rules as components:
|
|
700
|
+
no inline styles, tokens only);
|
|
701
|
+
- `<domain>-<name>.types.ts` — only when the page actually has props;
|
|
702
|
+
pages normally take none (route params come via `useNavigationParams()`).
|
|
703
|
+
- The domain barrel `src/pages/<domain>/index.ts` contains only
|
|
704
|
+
`export * from './<domain>-<name>'` lines, sorted alphabetically — like
|
|
705
|
+
every other barrel.
|
|
706
|
+
|
|
707
|
+
### The page owns the screen container
|
|
708
|
+
|
|
709
|
+
**Every page's root element is a screen-scaffold template — never anything
|
|
710
|
+
else.** The page's `return` is exactly one of:
|
|
711
|
+
|
|
712
|
+
- `ScreenContainerScroll` / `ScreenContainerList` / `ScreenContainerSectionList`
|
|
713
|
+
(`ScreenContainer*`) — a normal screen;
|
|
714
|
+
- `ModalContainerScroll` / `ModalContainerList` / `ModalContainerSectionList`
|
|
715
|
+
(`ModalContainer*`) — a modal/sheet page;
|
|
716
|
+
- `WebContainer` — a WebView page;
|
|
717
|
+
- `ScreenAuth` — the auth-screen scaffold (itself a `ScreenContainerScroll`
|
|
718
|
+
underneath), used only by auth pages.
|
|
719
|
+
|
|
720
|
+
A page whose root is a widget (`<SomeWidget />`), a feature, an entity, a raw
|
|
721
|
+
`View`, or a fragment (`<>…</>`) is a **defect**. Widgets, features, entities,
|
|
722
|
+
and shared UI are always composed _inside_ the container, never rendered as the
|
|
723
|
+
screen root:
|
|
724
|
+
|
|
725
|
+
```tsx
|
|
726
|
+
// ✅ page owns the container; the widget is the inner content
|
|
727
|
+
const ProfileEdit = () => (
|
|
728
|
+
<ScreenContainerScroll screenHeader={<ScreenHeader left={<ScreenHeaderIconBack />} />}>
|
|
729
|
+
<ProfileEditForm />
|
|
730
|
+
</ScreenContainerScroll>
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
// ❌ widget is the screen root — the container is hidden inside the widget
|
|
734
|
+
const ProfileEdit = () => <ProfileEditForm />
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**The container and its chrome live in the page, not in a widget.** The
|
|
738
|
+
`ScreenContainer*` element — together with its `screenHeader` / `footer` /
|
|
739
|
+
`onRefresh` / list props — belongs to the page. A widget that renders its own
|
|
740
|
+
`ScreenContainer*` (owning the header, footer, refresh, or the `FlatList`
|
|
741
|
+
itself) is the same defect from the other side: it turns the widget _into_ the
|
|
742
|
+
screen and leaves the page an empty pass-through. Widgets render inner content
|
|
743
|
+
only.
|
|
744
|
+
|
|
745
|
+
When the header/footer/refresh are data-driven, the page calls the owning hook
|
|
746
|
+
and wires the container from it — passing what the inner widget needs down as
|
|
747
|
+
props, so there is a **single** hook instance. Never duplicate a hook that
|
|
748
|
+
holds local form state (fields would desync from the footer's submit); a hook
|
|
749
|
+
that is purely query/store-backed may be re-read anywhere, since the cache is
|
|
750
|
+
shared.
|
|
751
|
+
|
|
752
|
+
- Heavy markup belongs to widgets; the page composes them inside the container.
|
|
753
|
+
|
|
754
|
+
### Pull-to-refresh
|
|
755
|
+
|
|
756
|
+
**Every screen that shows refetchable server data wires pull-to-refresh** — the
|
|
757
|
+
component that owns the data (the page, or the widget the page composes) passes
|
|
758
|
+
`onRefresh` to its `ScreenContainer*` root. See `handleRefresh` in
|
|
759
|
+
`references/pages/post/post-list/` and `references/hooks/use-posts.ts`.
|
|
760
|
+
|
|
761
|
+
- The handler follows the standard shape: `handleRefresh` in `useCallback` that
|
|
762
|
+
`await`s the query's `refetch`. When the page reads data through a domain
|
|
763
|
+
hook, that hook exposes `handleRefresh` alongside the data.
|
|
764
|
+
- The shared `RefreshControl` tracks its own spinner while the returned promise
|
|
765
|
+
is pending — the page never keeps an `isRefreshing` state and never renders a
|
|
766
|
+
`RefreshControl` manually.
|
|
767
|
+
- A screen composed of several queries refreshes **all** of them:
|
|
768
|
+
`await Promise.all([refetchA(), refetchB()])`.
|
|
769
|
+
- Out of scope: screens without server data (static menus, purely local forms)
|
|
770
|
+
and `ModalContainer*` modals — the modal templates deliberately do not
|
|
771
|
+
support pull-to-refresh.
|
|
772
|
+
|
|
773
|
+
## Forms: keyboard UX
|
|
774
|
+
|
|
775
|
+
Every form with **two or more text inputs** ships the standard keyboard flow —
|
|
776
|
+
don't reinvent it per form:
|
|
777
|
+
|
|
778
|
+
- **autofocus** of the first field after the screen transition animation
|
|
779
|
+
finishes (the keyboard must not fight the native navigation animation);
|
|
780
|
+
- **Enter → next field** (focus moves, keyboard stays up);
|
|
781
|
+
- **Enter on the last field → submit** — the same handler as the form's main
|
|
782
|
+
button.
|
|
783
|
+
|
|
784
|
+
The primitives already exist: the `useFormFields` hook (`src/shared/hooks`),
|
|
785
|
+
the extended `Input` (props `returnKeyType` / `submitBehavior` / `onSubmit`),
|
|
786
|
+
and `useNavigationTransitionEnd`, which defers autofocus until the native
|
|
787
|
+
transition ends (`autoFocus`, `setTimeout`, and `InteractionManager` all fire
|
|
788
|
+
during the transition — only the navigation `transitionEnd` signal is
|
|
789
|
+
reliable).
|
|
790
|
+
|
|
791
|
+
```tsx
|
|
792
|
+
const handleSubmit = useCallback(() => {
|
|
793
|
+
// the form's main action (request, navigation, …)
|
|
794
|
+
}, [])
|
|
795
|
+
|
|
796
|
+
const { getFieldProps } = useFormFields(2, { onSubmit: handleSubmit })
|
|
797
|
+
|
|
798
|
+
<Input {...getFieldProps(0)} value={email} onChange={setEmail} />
|
|
799
|
+
<Input {...getFieldProps(1)} value={password} onChange={setPassword} isSecure={true} />
|
|
800
|
+
<Button title={t('actions.sign_in')} type={'base'} onPress={handleSubmit} />
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
- `getFieldProps(index)` hands the field `ref`, `shouldFocus`,
|
|
804
|
+
`returnKeyType`, `submitBehavior`, and `onSubmit`; the field stays fully
|
|
805
|
+
controlled via its own `value` / `onChange` / `label`.
|
|
806
|
+
- Spread `{...getFieldProps(index)}` **first** among the `Input` props so
|
|
807
|
+
explicit props can override the navigation ones.
|
|
808
|
+
- `index` is the visual order (0-based, top to bottom) of the **actually
|
|
809
|
+
rendered** fields; keep `fieldCount` and indices in sync with the render.
|
|
810
|
+
Dynamically shown fields are simpler left unwired.
|
|
811
|
+
- The submit button and the last field call **the same** handler.
|
|
812
|
+
- Autofocus is on by default; disable with `{ shouldAutoFocus: false }` when
|
|
813
|
+
the user should read the screen first.
|
|
814
|
+
- Non-`TextInput` fields (`input-select`, `input-phone`, `input-code`) don't
|
|
815
|
+
take these props — use their own focus/submit mechanisms.
|
|
816
|
+
- A single field needs no form hook: `shouldFocus={true}` directly, or, if
|
|
817
|
+
immediate focus fights the transition,
|
|
818
|
+
`useNavigationTransitionEnd(() => ref.current?.focus())`.
|
|
819
|
+
|
|
820
|
+
## Translations
|
|
821
|
+
|
|
822
|
+
`src/shared/assets/translations/locales/` is **generated and gitignored —
|
|
823
|
+
never edit it by hand and never commit it.** The source of truth is the web
|
|
824
|
+
app: `../front/lang/<code>.json`, and **only the `mobile` section** of each
|
|
825
|
+
file. `scripts/transfer-translations.mjs` copies `JSON.parse(file).mobile`
|
|
826
|
+
into `.ts` modules; regenerate any time with **`npm run translations`** (also
|
|
827
|
+
runs on `postinstall`).
|
|
828
|
+
|
|
829
|
+
### Hard rules
|
|
830
|
+
|
|
831
|
+
1. **New translations go to Russian only** — `../front/lang/ru.json`, under
|
|
832
|
+
`mobile.*`, and nowhere else. Never add or translate the other languages;
|
|
833
|
+
those files are parser input.
|
|
834
|
+
2. **Never edit anything outside the `mobile` property.** The rest of every
|
|
835
|
+
web `lang` file belongs to the web app — not our zone, under any
|
|
836
|
+
circumstance.
|
|
837
|
+
3. **Do not edit existing web translations on your own.** Only when the user
|
|
838
|
+
explicitly asks, and always confirm the exact key + new text before
|
|
839
|
+
writing.
|
|
840
|
+
4. **Reuse before adding.** Prefer an existing key (`actions.*`,
|
|
841
|
+
`elements.*`, an existing `screens.*`) over a new one. When a string
|
|
842
|
+
genuinely has no key, collect the proposed `key → Russian text` list and
|
|
843
|
+
get the user's confirmation before adding.
|
|
844
|
+
5. **Regenerate after any web change** (`npm run translations`), then wire
|
|
845
|
+
the key(s) in code.
|
|
846
|
+
|
|
847
|
+
### The `mobile.` prefix
|
|
848
|
+
|
|
849
|
+
The script strips the `mobile` wrapper: the key in code has **no `mobile.`
|
|
850
|
+
prefix**, the path in the web file (and in the change report) **has it**.
|
|
851
|
+
Web file: `mobile.screens.auth.sign_in.separator` → code:
|
|
852
|
+
`t('screens.auth.sign_in.separator')`.
|
|
853
|
+
|
|
854
|
+
### Finding and structuring keys
|
|
855
|
+
|
|
856
|
+
Top-level domains under `mobile`: `screens.<feature>.<screen>.*` (copy scoped
|
|
857
|
+
to one screen), `navigation.*` (tab/section names), `actions.*` (reusable
|
|
858
|
+
button labels — check here first for buttons), `elements.*` (reusable UI
|
|
859
|
+
bits: `elements.fields.<field>.{title,placeholder,…}`, `elements.labels.*`,
|
|
860
|
+
`elements.modals.*`, …), `errors.*` / `validation.*`. Don't memorize keys —
|
|
861
|
+
grep `../front/lang/ru.json` for a distinctive substring of the Russian text.
|
|
862
|
+
|
|
863
|
+
### Conventions
|
|
864
|
+
|
|
865
|
+
- Keys and interpolation variables are **snake_case**; interpolation is
|
|
866
|
+
i18next `{{var}}`: `t('…pending_card_title', { currency })`.
|
|
867
|
+
- Rich text (links/bold inside a sentence) uses angle-bracket tags rendered
|
|
868
|
+
by `LocalizationTranslate` (re-export of i18next `Trans`) with a
|
|
869
|
+
`components` map keyed by tag name (`<link-terms>…</link-terms>`).
|
|
870
|
+
- In code: `const t = useLocalizationText()`, then `t('actions.sign_in')`.
|
|
871
|
+
|
|
872
|
+
### Change report — mandatory
|
|
873
|
+
|
|
874
|
+
After any task that changed the web files, end the answer with one block per
|
|
875
|
+
changed key, using the full web path (with `mobile.`):
|
|
876
|
+
|
|
877
|
+
```
|
|
878
|
+
Ключ "mobile.screens.balance.sum"
|
|
879
|
+
Новый перевод: "Сумма, {{currency}}"
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
## Async
|
|
883
|
+
|
|
884
|
+
Asynchronous code uses `async/await` — `.then()` / `.catch()` / `.finally()`
|
|
885
|
+
chains are not used. Promise chains nest poorly, hide control flow, and split
|
|
886
|
+
error handling across callbacks; `await` reads top-to-bottom like sync code.
|
|
887
|
+
|
|
888
|
+
- Always `await` and wrap fallible calls in `try/catch` (use `catch {` without a
|
|
889
|
+
binding when the error is unused), never `.then(onOk, onErr)` or
|
|
890
|
+
`.catch(...)`.
|
|
891
|
+
- A sync callback that must return a value (an unsubscribe, a cleanup) but also
|
|
892
|
+
needs to kick off async work declares a local `async` helper and calls it —
|
|
893
|
+
the floating call is fine, no `void` operator needed:
|
|
894
|
+
|
|
895
|
+
```ts
|
|
896
|
+
someListener((set) => {
|
|
897
|
+
const sync = async () => {
|
|
898
|
+
try {
|
|
899
|
+
set(await read())
|
|
900
|
+
} catch {
|
|
901
|
+
set(fallback)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
sync()
|
|
906
|
+
|
|
907
|
+
return () => unsubscribe()
|
|
908
|
+
})
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
- Run independent awaits concurrently with `await Promise.all([...])`, not
|
|
912
|
+
sequential `await`s.
|
|
913
|
+
|
|
914
|
+
## Documentation and comments
|
|
915
|
+
|
|
916
|
+
JSDoc on the public surface is **mandatory** on every file; ordinary inline comments stay
|
|
917
|
+
discouraged.
|
|
918
|
+
|
|
919
|
+
**Language:** every JSDoc block is written in **Russian**. Code identifiers (names of
|
|
920
|
+
types, functions, props, constants) keep their original form — translate the prose, never
|
|
921
|
+
the symbols.
|
|
922
|
+
|
|
923
|
+
### JSDoc — required on the public surface, in Russian
|
|
924
|
+
|
|
925
|
+
A `/** … */` block sits immediately above every declaration on a file's public /
|
|
926
|
+
top-level surface:
|
|
927
|
+
|
|
928
|
+
- exported and top-level functions, hooks, components;
|
|
929
|
+
- classes, and each of their methods and fields;
|
|
930
|
+
- exported and top-level types and constants.
|
|
931
|
+
|
|
932
|
+
```ts
|
|
933
|
+
/** Форматирует сумму в локали пользователя; откатывается к сырому числу. */
|
|
934
|
+
const formatPrice = (amount: number, locale: Locale): string => { … }
|
|
935
|
+
|
|
936
|
+
/** Постраничный срез результатов плюс курсор на следующую страницу. */
|
|
937
|
+
type Page<Item> = {
|
|
938
|
+
/** Элементы текущей страницы. */
|
|
939
|
+
itemList: Item[]
|
|
940
|
+
/** Курсор следующей страницы; отсутствует на последней. */
|
|
941
|
+
nextCursor?: string
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
- Document intent (what-for / why), not a restatement of the signature. Add `@param` /
|
|
946
|
+
`@returns` only when they carry information the types do not.
|
|
947
|
+
- A single-line `/** … */` is enough when one line says it; expand only when it earns it.
|
|
948
|
+
- Local variables inside a function body get **no** JSDoc.
|
|
949
|
+
|
|
950
|
+
### Inline comments stay discouraged
|
|
951
|
+
|
|
952
|
+
Inside a function body, prefer renaming or extracting over a `//` explanation — the code
|
|
953
|
+
must read declaratively. Sanctioned non-JSDoc inline markers remain:
|
|
954
|
+
|
|
955
|
+
- Figma variant markers in typography styles (`/* big-title */`), kept solely so
|
|
956
|
+
variants are findable by search.
|
|
957
|
+
- `// auth` / `// no auth` markers on repository methods in the shared API layer
|
|
958
|
+
(written by the api skill) and the `// config` / `// response` section markers in
|
|
959
|
+
repository types files.
|
|
960
|
+
|
|
961
|
+
## Quick self-check before finishing
|
|
962
|
+
|
|
963
|
+
- Arrays end with `List` (generic list components may keep `data: Item[]`);
|
|
964
|
+
booleans start with `is/can/did/has/should/with` (controlled selection
|
|
965
|
+
components keep `value?: boolean`)
|
|
966
|
+
- Dictionaries plural, counters `*Count`, design constants UPPER_SNAKE
|
|
967
|
+
- Files/folders kebab-case; exports collected at end of file; default export
|
|
968
|
+
only for components
|
|
969
|
+
- `@mediacubeco` utilities used via subpath imports; nothing re-implemented;
|
|
970
|
+
optional callbacks never called via `?.()`
|
|
971
|
+
- Visual logic in `useXxxStyles`; no inline styles; tokens, never hardcoded
|
|
972
|
+
visual values; theme and measurement hooks never called in the component
|
|
973
|
+
file; styles hook takes no arguments when it can (finite variants built
|
|
974
|
+
inside, picked by key), a context only for non-enumerable runtime values
|
|
975
|
+
- Colors by role with alpha steps; accent via `PRIMARY*`; paired contrast
|
|
976
|
+
tokens; radii and spacing from `SIZES` (computed `UPPER_SNAKE_CASE`
|
|
977
|
+
constants with JSDoc; cards at `5XL`+; rhythm table respected)
|
|
978
|
+
- Text only through the `Text` atom's `type` scale; a text skeleton is as tall
|
|
979
|
+
as the replaced type's line box (`fontSize` token × 1.2), never raw pixels;
|
|
980
|
+
`Glass`/`Blur` per the materials rules; shadows from `SHADOWS`
|
|
981
|
+
- Animation configs (when passed) only via `ANIMATIONS` tokens; `Touch`
|
|
982
|
+
feedback prop by role (`withRipple` / `withSpring` / `withHaptics`);
|
|
983
|
+
disabled via `DISABLED` tokens + `isDisabled`; selection UIs follow
|
|
984
|
+
`references/components/payout-speed-option/`
|
|
985
|
+
- New component: 4-file folder at the right atomic layer, explicit `index.ts`
|
|
986
|
+
re-exports, registered alphabetically in the layer barrel
|
|
987
|
+
- Every handler in `useCallback`; `useMemo`/`memo` only where expensive
|
|
988
|
+
- New hook: `use-<name>.ts` in the right `hooks/` folder, registered in its
|
|
989
|
+
barrel; multi-value hooks return one object, single-value hooks return the
|
|
990
|
+
value; no hand-written return types
|
|
991
|
+
- New store: Zustand per the State section — slice `model/` (or
|
|
992
|
+
`src/shared/lib/store/` for cross-cutting), selectors at call sites,
|
|
993
|
+
persist only what must survive a restart
|
|
994
|
+
- New page: own folder `src/pages/<domain>/<domain>-<name>/` with `index.ts`
|
|
995
|
+
(+ `.styles.ts` for visual logic); domain barrel alphabetical; no flat page
|
|
996
|
+
files
|
|
997
|
+
- Page root is a screen scaffold (`ScreenContainer*` / `ModalContainer*` /
|
|
998
|
+
`WebContainer` / `ScreenAuth`) — never a widget/feature/entity/`View`/fragment;
|
|
999
|
+
the container and its `screenHeader` / `footer` / `onRefresh` live in the page,
|
|
1000
|
+
never inside a widget; a data-driven container calls one hook instance (props
|
|
1001
|
+
down to the widget — no duplicating a local-state form hook)
|
|
1002
|
+
- Flow screens: back (`ScreenHeaderIconBack`, `left`) on every step but the last;
|
|
1003
|
+
the terminal screen uses close (`ScreenHeaderIconClose to={…}`, `right`,
|
|
1004
|
+
`dismissTo` the flow's first screen) with no back
|
|
1005
|
+
- `src/app` files stay thin wrappers (`Page` / `Layout`) — no logic, no extra
|
|
1006
|
+
dependencies; sheets via `sheetOptions`, web screens via `webOptions`
|
|
1007
|
+
- Screen with server data passes `onRefresh` (a `handleRefresh` awaiting
|
|
1008
|
+
`refetch`) to its `ScreenContainer*`; no manual `isRefreshing` state
|
|
1009
|
+
- Form with 2+ text inputs wired through `useFormFields` (autofocus after
|
|
1010
|
+
transition, Enter chain, last field submits)
|
|
1011
|
+
- Translations only in `../front/lang/ru.json` (namespace `mobile`, Russian
|
|
1012
|
+
only), then `npm run translations`; generated assets never edited by hand;
|
|
1013
|
+
change report with full `mobile.*` paths
|
|
1014
|
+
- JSX props braced and explicit; `&&` for single-branch rendering, ternary
|
|
1015
|
+
only for a real two-way choice; prop order respected
|
|
1016
|
+
- Object shapes via `type` (no `interface`); fixed constant sets via string
|
|
1017
|
+
`enum` (PascalCase members); constant lookup maps typed by `as const`, not an
|
|
1018
|
+
explicit `Record<…>` annotation
|
|
1019
|
+
- `ref` as a plain prop; never `forwardRef`
|
|
1020
|
+
- `async/await` with `try/catch` everywhere; no `.then()`/`.catch()` chains, no
|
|
1021
|
+
`void` operator
|
|
1022
|
+
- JSDoc (Russian) on every public/top-level declaration and every class member; intent
|
|
1023
|
+
over signature restatement
|
|
1024
|
+
- No explanatory inline `//` inside bodies (Figma `/* … */` markers and API-layer
|
|
1025
|
+
`// auth` / section markers excepted)
|