@@ -4,152 +4,51 @@ import { LinearGradient } from 'expo-linear-gradient';
import { Stack , useRouter } from 'expo-router' ;
import React , { useEffect , useMemo , useRef , useState } from 'react' ;
import {
DimensionValue ,
FlatList ,
StyleSheet ,
Text ,
TouchableOpacity ,
View ,
} from 'react-native' ;
import { useTranslation } from 'react-i18next' ;
import { Colors } from '@/constants/Colors ';
import { InlineTip , ITEM_HEIGHT , Legend , MonthBlock } from '@/components/menstrual-cycle ';
import {
deleteMenstrualFlow ,
fetchMenstrualFlowSamples ,
saveMenstrualFlow
} from '@/utils/health' ;
import {
buildMenstrualTimeline ,
convertHealthKitSamplesToCycleRecords ,
CycleRecord ,
DEFAULT_PERIOD_LENGTH ,
MenstrualDayCell ,
MenstrualDayStatus ,
MenstrualTimeline ,
buildMenstrualTimeline
DEFAULT_PERIOD_LENGTH
} from '@/utils/menstrualCycle' ;
type TabKey = 'cycle' | 'analysis' ;
const ITEM_HEIGHT = 380 ;
const STATUS_COLORS : Record < MenstrualDayStatus , { bg : string ; text : string } > = {
period : { bg : '#f5679f' , text : '#fff' } ,
'predicted-period' : { bg : '#f8d9e9' , text : '#9b2c6a' } ,
fertile : { bg : '#d9d2ff' , text : '#5a52c5' } ,
'ovulation-day' : { bg : '#5b4ee4' , text : '#fff' } ,
} ;
const WEEK_LABELS = [ '一' , '二' , '三' , '四' , '五' , '六' , '日' ] ;
const chunkArray = < T , > ( array : T [ ] , size : number ) : T [ ] [ ] = > {
const result : T [ ] [ ] = [ ] ;
for ( let i = 0 ; i < array . length ; i += size ) {
result . push ( array . slice ( i , i + size ) ) ;
}
return result ;
} ;
const DayCell = ( {
cell ,
isSelected ,
onPress ,
} : {
cell : Extract < MenstrualDayCell , { type : 'day' } > ;
isSelected : boolean ;
onPress : ( ) = > void ;
} ) = > {
const status = cell . info ? . status ;
const colors = status ? STATUS_COLORS [ status ] : undefined ;
return (
< TouchableOpacity
activeOpacity = { 0.8 }
style = { styles . dayCell }
onPress = { onPress }
>
< View
style = { [
styles . dayCircle ,
colors && { backgroundColor : colors.bg } ,
isSelected && styles . dayCircleSelected ,
cell . isToday && styles . todayOutline ,
] }
>
< Text
style = { [
styles . dayLabel ,
colors && { color : colors.text } ,
! colors && styles . dayLabelDefault ,
] }
>
{ cell . label }
< / Text >
< / View >
{ cell . isToday && < Text style = { styles . todayText } > 今 天 < / Text > }
< / TouchableOpacity >
) ;
} ;
const MonthBlock = ( {
month ,
selectedDateKey ,
onSelect ,
renderTip ,
} : {
month : MenstrualTimeline [ 'months' ] [ number ] ;
selectedDateKey : string ;
onSelect : ( dateKey : string ) = > void ;
renderTip : ( colIndex : number ) = > React . ReactNode ;
} ) = > {
const weeks = useMemo ( ( ) = > chunkArray ( month . cells , 7 ) , [ month . cells ] ) ;
return (
< View style = { styles . monthCard } >
< View style = { styles . monthHeader } >
< Text style = { styles . monthTitle } > { month . title } < / Text >
< Text style = { styles . monthSubtitle } > { month . subtitle } < / Text >
< / View >
< View style = { styles . weekRow } >
{ WEEK_LABELS . map ( ( label ) = > (
< Text key = { label } style = { styles . weekLabel } >
{ label }
< / Text >
) ) }
< / View >
< View style = { styles . monthGrid } >
{ weeks . map ( ( week , weekIndex ) = > {
const selectedIndex = week . findIndex (
( c ) = > c . type === 'day' && c . date . format ( 'YYYY-MM-DD' ) === selectedDateKey
) ;
return (
< React.Fragment key = { weekIndex } >
< View style = { styles . daysRow } >
{ week . map ( ( cell ) = > {
if ( cell . type === 'placeholder' ) {
return < View key = { cell . key } style = { styles . dayCell } / > ;
}
const dateKey = cell . date . format ( 'YYYY-MM-DD' ) ;
return (
< DayCell
key = { cell . key }
cell = { cell }
isSelected = { selectedDateKey === dateKey }
onPress = { ( ) = > onSelect ( dateKey ) }
/ >
) ;
} ) }
< / View >
{ selectedIndex !== - 1 && (
< View style = { styles . inlineTipContainer } >
{ renderTip ( selectedIndex ) }
< / View >
) }
< / React.Fragment >
) ;
} ) }
< / View >
< / View >
) ;
} ;
export default function MenstrualCycleScreen() {
const router = useRouter ( ) ;
const { t } = useTranslation ( ) ;
const [ records , setRecords ] = useState < CycleRecord [ ] > ( [ ] ) ;
const [ windowConfig , setWindowConfig ] = useState ( { before : 2 , after : 3 } ) ;
// Load data from HealthKit
useEffect ( ( ) = > {
const loadData = async ( ) = > {
// Calculate date range based on windowConfig
const today = dayjs ( ) ;
const startDate = today . subtract ( windowConfig . before , 'month' ) . startOf ( 'month' ) . toDate ( ) ;
const endDate = today . add ( windowConfig . after , 'month' ) . endOf ( 'month' ) . toDate ( ) ;
const samples = await fetchMenstrualFlowSamples ( startDate , endDate ) ;
const convertedRecords = convertHealthKitSamplesToCycleRecords ( samples ) ;
setRecords ( convertedRecords ) ;
} ;
loadData ( ) ;
} , [ windowConfig ] ) ;
const timeline = useMemo (
( ) = >
buildMenstrualTimeline ( {
@@ -173,10 +72,10 @@ export default function MenstrualCycleScreen() {
const selectedDate = dayjs ( selectedDateKey ) ;
const handleMarkStart = ( ) = > {
const handleMarkStart = async ( ) = > {
if ( selectedDate . isAfter ( dayjs ( ) , 'day' ) ) return ;
// Check if the selected date is already covered by an existing record (including duration)
// Check if the selected date is already covered
const isCovered = records . some ( ( r ) = > {
const start = dayjs ( r . startDate ) ;
const end = start . add ( ( r . periodLength ? ? DEFAULT_PERIOD_LENGTH ) - 1 , 'day' ) ;
@@ -187,45 +86,36 @@ export default function MenstrualCycleScreen() {
} ) ;
if ( isCovered ) return ;
// Optimistic Update
const originalRecords = [ . . . records ] ;
setRecords ( ( prev ) = > {
const updated = [ . . . prev ] ;
// 1. Check if selectedDate is immediately after an existing period
// Logic for optimistic UI update (same as original logic)
const prevRecordIndex = updated . findIndex ( ( r ) = > {
const start = dayjs ( r . startDate ) ;
const end = start . add ( ( r . periodLength ? ? DEFAULT_PERIOD_LENGTH ) - 1 , 'day' ) ;
return end . add ( 1 , 'day' ) . isSame ( selectedDate , 'day' ) ;
} ) ;
// 2. Check if selectedDate is immediately before an existing period
const nextRecordIndex = updated . findIndex ( ( r ) = > {
return dayjs ( r . startDate ) . subtract ( 1 , 'day' ) . isSame ( selectedDate , 'day' ) ;
} ) ;
if ( prevRecordIndex !== - 1 && nextRecordIndex !== - 1 ) {
// Merge three parts: Prev + Selected + Next
const prevRecord = updated [ prevRecordIndex ] ;
const nextRecord = updated [ nextRecordIndex ] ;
const newLength =
( prevRecord . periodLength ? ? DEFAULT_PERIOD_LENGTH ) +
1 +
( nextRecord . periodLength ? ? DEFAULT_PERIOD_LENGTH ) ;
updated [ prevRecordIndex ] = {
. . . prevRecord ,
periodLength : newLength ,
} ;
// Remove the next record since it's merged
updated [ prevRecordIndex ] = { . . . prevRecord , periodLength : newLength } ;
updated . splice ( nextRecordIndex , 1 ) ;
} else if ( prevRecordIndex !== - 1 ) {
// Extend previous record
const prevRecord = updated [ prevRecordIndex ] ;
updated [ prevRecordIndex ] = {
. . . prevRecord ,
periodLength : ( prevRecord . periodLength ? ? DEFAULT_PERIOD_LENGTH ) + 1 ,
} ;
} else if ( nextRecordIndex !== - 1 ) {
// Extend next record (start earlier)
const nextRecord = updated [ nextRecordIndex ] ;
updated [ nextRecordIndex ] = {
. . . nextRecord ,
@@ -233,7 +123,6 @@ export default function MenstrualCycleScreen() {
periodLength : ( nextRecord . periodLength ? ? DEFAULT_PERIOD_LENGTH ) + 1 ,
} ;
} else {
// Create new isolated record
const newRecord : CycleRecord = {
startDate : selectedDate.format ( 'YYYY-MM-DD' ) ,
periodLength : 7 ,
@@ -241,18 +130,59 @@ export default function MenstrualCycleScreen() {
} ;
updated . push ( newRecord ) ;
}
return updated . sort (
( a , b ) = > dayjs ( a . startDate ) . valueOf ( ) - dayjs ( b . startDate ) . valueOf ( )
) ;
return updated . sort ( ( a , b ) = > dayjs ( a . startDate ) . valueOf ( ) - dayjs ( b . startDate ) . valueOf ( ) ) ;
} ) ;
try {
// Determine what to save to HealthKit
// If we are merging or extending, we are effectively adding one day of flow
// If we are creating a new record, we default to 7 days
// However, accurate HealthKit logging should be per day.
// The previous UI logic "creates" a 7-day period for a single tap.
// We should replicate this behavior in HealthKit for consistency.
const isNewIsolatedRecord = ! records . some ( ( r ) = > {
const start = dayjs ( r . startDate ) ;
const end = start . add ( ( r . periodLength ? ? DEFAULT_PERIOD_LENGTH ) - 1 , 'day' ) ;
// Check adjacency
return (
end . add ( 1 , 'day' ) . isSame ( selectedDate , 'day' ) ||
dayjs ( r . startDate ) . subtract ( 1 , 'day' ) . isSame ( selectedDate , 'day' )
) ;
} ) ;
if ( isNewIsolatedRecord ) {
// Save 7 days of flow starting from selectedDate
const promises = [ ] ;
for ( let i = 0 ; i < 7 ; i ++ ) {
const date = selectedDate . add ( i , 'day' ) ;
// Don't save future dates if they exceed today (though logic allows predicting)
// But for flow logging, we usually only log past/present.
// However, UI allows setting a period that might extend slightly?
// Let's stick to the selected date logic.
// Wait, if I tap "Mark Start", it creates a 7 day period.
// Should I write 7 samples? Yes, to match the UI state.
promises . push ( saveMenstrualFlow ( date . toDate ( ) , 1 , i === 0 ) ) ; // 1=unspecified
}
await Promise . all ( promises ) ;
} else {
// Just adding a single day to bridge/extend
await saveMenstrualFlow ( selectedDate . toDate ( ) , 1 , false ) ;
}
} catch ( error ) {
console . error ( 'Failed to save to HealthKit' , error ) ;
// Revert optimistic update
setRecords ( originalRecords ) ;
}
} ;
const handleCancelMark = ( ) = > {
const handleCancelMark = async ( ) = > {
if ( ! selectedInfo || ! selectedInfo . confirmed ) return ;
if ( selectedDate . isAfter ( dayjs ( ) , 'day' ) ) return ;
const target = selectedDate ;
// Optimistic Update
const originalRecords = [ . . . records ] ;
setRecords ( ( prev ) = > {
const updated : CycleRecord [ ] = [ ] ;
prev . forEach ( ( record ) = > {
@@ -264,21 +194,47 @@ export default function MenstrualCycleScreen() {
updated . push ( record ) ;
return ;
}
if ( diff === 0 ) {
// 取消开始日:移除整段记录
return ;
}
// diff > 0 且在区间内:将该日标记为结束日 (选中当日也被取消,所以长度为 diff)
updated . push ( {
. . . record ,
periodLength : diff ,
} ) ;
if ( diff === 0 ) return ; // Remove entire record (or start of it)
updated . push ( { . . . record , periodLength : diff } ) ; // Shorten it
} ) ;
return updated ;
} ) ;
try {
// Logic:
// 1. Find the record covering the target date
const record = records . find ( ( r ) = > {
const start = dayjs ( r . startDate ) ;
const end = start . add ( ( r . periodLength ? ? DEFAULT_PERIOD_LENGTH ) - 1 , 'day' ) ;
return (
( target . isSame ( start , 'day' ) || target . isAfter ( start , 'day' ) ) &&
( target . isSame ( end , 'day' ) || target . isBefore ( end , 'day' ) )
) ;
} ) ;
if ( record ) {
const start = dayjs ( record . startDate ) ;
const diff = target . diff ( start , 'day' ) ;
if ( diff === 0 ) {
// If cancelling the start date, the UI removes the ENTIRE period record.
// So we should delete all samples for this period range.
const periodLength = record . periodLength ? ? DEFAULT_PERIOD_LENGTH ;
const endDate = start . add ( periodLength - 1 , 'day' ) ;
await deleteMenstrualFlow ( start . toDate ( ) , endDate . toDate ( ) ) ;
} else {
// If cancelling a middle/end date, the UI shortens the period to end BEFORE target.
// So we delete from target date onwards to the original end date.
const periodLength = record . periodLength ? ? DEFAULT_PERIOD_LENGTH ;
const originalEnd = start . add ( periodLength - 1 , 'day' ) ;
// Delete from target to originalEnd
await deleteMenstrualFlow ( target . toDate ( ) , originalEnd . toDate ( ) ) ;
}
}
} catch ( error ) {
console . error ( 'Failed to delete from HealthKit' , error ) ;
setRecords ( originalRecords ) ;
}
} ;
const handleLoadPrevious = ( ) = > {
@@ -314,27 +270,7 @@ export default function MenstrualCycleScreen() {
}
} ) . current ;
const renderLegend = ( ) = > (
< View style = { styles . legendRow } >
{ [
{ label : '经期' , key : 'period' as const } ,
{ label : '预测经期' , key : 'predicted-period' as const } ,
{ label : '排卵期' , key : 'fertile' as const } ,
{ label : '排卵日' , key : 'ovulation-day' as const } ,
] . map ( ( item ) = > (
< View key = { item . key } style = { styles . legendItem } >
< View
style = { [
styles . legendDot ,
{ backgroundColor : STATUS_COLORS [ item . key ] . bg } ,
item . key === 'ovulation-day' && styles . legendDotRing ,
] }
/ >
< Text style = { styles . legendLabel } > { item . label } < / Text >
< / View >
) ) }
< / View >
) ;
const listData = useMemo ( ( ) = > {
return timeline . months . map ( ( m ) = > ( {
@@ -344,40 +280,19 @@ export default function MenstrualCycleScreen() {
} ) ) ;
} , [ timeline . months ] ) ;
const renderInlineTip = ( columnIndex : number ) = > {
// 14.28% per cell. Center is 7.14%.
const pointerLeft = ` ${ columnIndex * 14.2857 + 7.1428 } % ` as DimensionValu e;
const isFuture = selectedDate . isAfter ( dayjs ( ) , 'day' ) ;
const base = (
< View style = { styles . inlineTipC ard } >
< View style = { [ styles . inlineTipPointer , { left : pointerLeft } ] } / >
< View style = { styles . inlineTipRow } >
< View style = { styles . inlineTipDate } >
< Ionicons name = "calendar-outline" size = { 16 } color = "#111827" / >
< Text style = { styles . inlineTipDateText } > { selectedDate . format ( 'M月D日' ) } < / Text >
< / View >
{ ! isFuture && ( ! selectedInfo || ! selectedInfo . confirmed ) && (
< TouchableOpacity style = { styles . inlinePrimaryBtn } onPress = { handleMarkStart } >
< Ionicons name = "add" size = { 14 } color = "#fff" / >
< Text style = { styles . inlinePrimaryText } > 标 记 经 期 < / Text >
< / TouchableOpacity >
) }
{ ! isFuture && selectedInfo ? . confirmed && selectedInfo . status === 'period' && (
< TouchableOpacity style = { styles . inlineSecondaryBtn } onPress = { handleCancelMark } >
< Text style = { styles . inlineSecondaryText } > 取 消 标 记 < / Text >
< / TouchableOpacity >
) }
< / View >
< / View >
) ;
return base ;
} ;
const renderInlineTip = ( columnIndex : number ) = > (
< InlineTip
selectedDate = { selectedDat e }
selectedInfo = { selectedInfo }
columnIndex = { columnIndex }
onMarkStart = { handleMarkStart }
onCancelMark = { handleCancelM ark }
/ >
) ;
const renderCycleTab = ( ) = > (
< View style = { styles . tabContent } >
{ renderLegend ( ) }
< Legend / >
< FlatList
@@ -411,9 +326,9 @@ export default function MenstrualCycleScreen() {
const renderAnalysisTab = ( ) = > (
< View style = { styles . tabContent } >
< View style = { styles . analysisCard } >
< Text style = { styles . analysisTitle } > 分 析 < / Text >
< Text style = { styles . analysisTitle } > { t ( 'menstrual.screen.analysis.title' ) } < / Text >
< Text style = { styles . analysisBody } >
基 于 最 近 6 个 周 期 的 记 录 , 计 算 平 均 经 期 和 周 期 长 度 , 后 续 会 展 示 趋 势 和 预 测 准 确 度 。
{ t ( 'menstrual.screen.analysis.description' ) }
< / Text >
< / View >
< / View >
@@ -433,7 +348,7 @@ export default function MenstrualCycleScreen() {
< TouchableOpacity onPress = { ( ) = > router . back ( ) } style = { styles . headerIcon } >
< Ionicons name = "chevron-back" size = { 22 } color = "#0f172a" / >
< / TouchableOpacity >
< Text style = { styles . headerTitle } > 生 理 周 期 < / Text >
< Text style = { styles . headerTitle } > { t ( 'menstrual.screen.header' ) } < / Text >
< TouchableOpacity style = { styles . headerIcon } >
< Ionicons name = "settings-outline" size = { 20 } color = "#0f172a" / >
< / TouchableOpacity >
@@ -441,8 +356,8 @@ export default function MenstrualCycleScreen() {
< View style = { styles . tabSwitcher } >
{ ( [
{ key : 'cycle' , label : '生理周期' } ,
{ key : 'analysis' , label : '分析' } ,
{ key : 'cycle' , label : t ( 'menstrual.screen.tabs.cycle' ) } ,
{ key : 'analysis' , label : t ( 'menstrual.screen.tabs.analysis' ) } ,
] as { key : TabKey ; label : string } [ ] ) . map ( ( tab ) = > {
const active = activeTab === tab . key ;
return (
@@ -525,32 +440,7 @@ const styles = StyleSheet.create({
tabContent : {
flex : 1 ,
} ,
legendRow : {
flexDirection : 'row' ,
flexWrap : 'wrap' ,
gap : 12 ,
marginBottom : 12 ,
paddingHorizontal : 4 ,
} ,
legendItem : {
flexDirection : 'row' ,
alignItems : 'center' ,
} ,
legendDot : {
width : 16 ,
height : 16 ,
borderRadius : 8 ,
marginRight : 6 ,
} ,
legendDotRing : {
borderWidth : 2 ,
borderColor : '#fff' ,
} ,
legendLabel : {
fontSize : 13 ,
color : '#111827' ,
fontFamily : 'AliRegular' ,
} ,
selectedCard : {
backgroundColor : '#fff' ,
borderRadius : 16 ,
@@ -569,206 +459,7 @@ const styles = StyleSheet.create({
fontWeight : '700' ,
fontFamily : 'AliBold' ,
} ,
tipCard : {
backgroundColor : '#f4f3ff' ,
borderRadius : 14 ,
padding : 12 ,
marginTop : 10 ,
borderWidth : 1 ,
borderColor : '#ede9fe' ,
} ,
tipTitle : {
fontSize : 14 ,
color : '#111827' ,
fontWeight : '700' ,
marginBottom : 4 ,
fontFamily : 'AliBold' ,
} ,
tipDesc : {
fontSize : 12 ,
color : '#6b7280' ,
lineHeight : 18 ,
marginBottom : 8 ,
fontFamily : 'AliRegular' ,
} ,
tipButton : {
backgroundColor : Colors.light.primary ,
paddingVertical : 10 ,
borderRadius : 12 ,
alignItems : 'center' ,
} ,
tipButtonText : {
color : '#fff' ,
fontSize : 14 ,
fontWeight : '700' ,
fontFamily : 'AliBold' ,
} ,
tipSecondaryButton : {
backgroundColor : '#fff' ,
paddingVertical : 10 ,
borderRadius : 12 ,
alignItems : 'center' ,
borderWidth : 1 ,
borderColor : '#e5e7eb' ,
} ,
tipSecondaryButtonText : {
color : '#0f172a' ,
fontSize : 14 ,
fontWeight : '700' ,
fontFamily : 'AliBold' ,
} ,
inlineTipCard : {
backgroundColor : '#e8e7ff' ,
borderRadius : 18 ,
paddingVertical : 10 ,
paddingHorizontal : 12 ,
shadowColor : '#000' ,
shadowOpacity : 0.04 ,
shadowRadius : 6 ,
shadowOffset : { width : 0 , height : 2 } ,
elevation : 1 ,
} ,
inlineTipPointer : {
position : 'absolute' ,
top : - 6 ,
width : 12 ,
height : 12 ,
marginLeft : - 6 ,
backgroundColor : '#e8e7ff' ,
transform : [ { rotate : '45deg' } ] ,
borderRadius : 3 ,
} ,
inlineTipRow : {
flexDirection : 'row' ,
alignItems : 'center' ,
justifyContent : 'space-between' ,
gap : 8 ,
} ,
inlineTipDate : {
flexDirection : 'row' ,
alignItems : 'center' ,
gap : 6 ,
} ,
inlineTipDateText : {
fontSize : 14 ,
color : '#111827' ,
fontWeight : '800' ,
fontFamily : 'AliBold' ,
} ,
inlinePrimaryBtn : {
flexDirection : 'row' ,
alignItems : 'center' ,
backgroundColor : Colors.light.primary ,
paddingHorizontal : 12 ,
paddingVertical : 8 ,
borderRadius : 14 ,
gap : 6 ,
} ,
inlinePrimaryText : {
color : '#fff' ,
fontSize : 13 ,
fontWeight : '700' ,
fontFamily : 'AliBold' ,
} ,
inlineSecondaryBtn : {
paddingHorizontal : 12 ,
paddingVertical : 8 ,
borderRadius : 14 ,
backgroundColor : '#fff' ,
borderWidth : 1 ,
borderColor : '#d1d5db' ,
} ,
inlineSecondaryText : {
color : '#111827' ,
fontSize : 13 ,
fontWeight : '700' ,
fontFamily : 'AliBold' ,
} ,
monthCard : {
backgroundColor : '#fff' ,
borderRadius : 16 ,
padding : 14 ,
marginBottom : 12 ,
shadowColor : '#000' ,
shadowOpacity : 0.08 ,
shadowRadius : 8 ,
shadowOffset : { width : 0 , height : 6 } ,
elevation : 2 ,
} ,
monthHeader : {
flexDirection : 'row' ,
alignItems : 'center' ,
justifyContent : 'space-between' ,
marginBottom : 8 ,
} ,
monthTitle : {
fontSize : 17 ,
fontWeight : '800' ,
color : '#0f172a' ,
fontFamily : 'AliBold' ,
} ,
monthSubtitle : {
fontSize : 12 ,
color : '#6b7280' ,
fontFamily : 'AliRegular' ,
} ,
weekRow : {
flexDirection : 'row' ,
justifyContent : 'space-between' ,
marginBottom : 6 ,
paddingHorizontal : 4 ,
} ,
weekLabel : {
width : '14.28%' ,
textAlign : 'center' ,
fontSize : 12 ,
color : '#94a3b8' ,
fontFamily : 'AliRegular' ,
} ,
monthGrid : {
flexDirection : 'column' ,
} ,
daysRow : {
flexDirection : 'row' ,
} ,
dayCell : {
width : '14.28%' ,
alignItems : 'center' ,
marginVertical : 6 ,
} ,
inlineTipContainer : {
paddingBottom : 6 ,
marginBottom : 6 ,
} ,
dayCircle : {
width : 40 ,
height : 40 ,
borderRadius : 20 ,
alignItems : 'center' ,
justifyContent : 'center' ,
backgroundColor : '#f3f4f6' ,
} ,
dayCircleSelected : {
borderWidth : 2 ,
borderColor : Colors.light.primary ,
} ,
todayOutline : {
borderWidth : 2 ,
borderColor : '#94a3b8' ,
} ,
dayLabel : {
fontSize : 15 ,
fontFamily : 'AliBold' ,
} ,
dayLabelDefault : {
color : '#111827' ,
} ,
todayText : {
fontSize : 10 ,
color : '#9ca3af' ,
marginTop : 2 ,
fontFamily : 'AliRegular' ,
} ,
listContent : {
paddingBottom : 80 ,
} ,